Merge branch 'refs/heads/main' into digest-refactor

# Conflicts:
#	ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs
This commit is contained in:
Slendy 2024-07-03 22:16:32 -05:00
commit 0e0a900f67
No known key found for this signature in database
GPG key ID: 7288D68361B91428
86 changed files with 1477 additions and 170 deletions

View file

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

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize shell scripts to have LF line endings
*.sh text eol=lf

89
.github/workflows/docker-publish.yml vendored Normal file
View file

@ -0,0 +1,89 @@
name: Build Docker Image
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build-publish:
name: Build and Publish
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 #v3.4.0
with:
cosign-release: 'v2.2.3'
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Hvis ikke, bedes du publicere kildekoden et eller andet sted, der er tilgængeligt for dine brugere.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Falls nicht, veröffentliche den Quellcode irgendwo, wo die Nutzer deiner Instanz darauf zugreifen können.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to yer users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Si no es así, por favor publique el código fuente en algún lugar accesible para sus usuarios.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>De lo contrario, publique el código fuente en algún lugar accesible para sus usuarios.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Jos ei, julkaise lähdekoodisi jonnekin, jossa se on käyttäjillesi saatavissa.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Kung hindi, mangyaring ilathala ang source code sa isang lugar na maaring ma-access ng iyong mga user.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Dans le cas contraire, veuillez publier le code source dans un endroit accessible à vos utilisateurs.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>In caso contrario, si prega di pubblicare il codice sorgente da qualche parte accessibile ai propri utenti.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Hvis ikke, vennligst publiser kildekoden et sted som er tilgjengelig for brukerne dine</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Jeśli nie, proszę opublikować kod źródłowy dostępny dla użytkowników.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Se não for o caso, publique o código-fonte em um local acessível aos seus utilizadores.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Если нет, пожалуйста, опубликуйте исходный код в доступном для ваших пользователей месте.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Om inte, vänligen publicera källkoden någonstans tillgängligt för dina användare.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Eğer değilse, lütfen kaynak kodunu kullanıcılarınızın erişebileceği bir yerde yayınlayın.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>Якщо ні, будь ласка, опублікуйте вихідний код у місці, доступному для ваших користувачів.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -80,4 +80,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -87,4 +87,10 @@
<data name="license_warn_3" xml:space="preserve">
<value>If not, please publish the source code somewhere accessible to your users.</value>
</data>
<data name="read_only_warn_title" xml:space="preserve">
<value>Read-Only Mode</value>
</data>
<data name="read_only_warn" xml:space="preserve">
<value>This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.</value>
</data>
</root>

View file

@ -23,5 +23,8 @@ public static class BaseLayoutStrings
public static readonly TranslatableString LicenseWarn2 = create("license_warn_2");
public static readonly TranslatableString LicenseWarn3 = create("license_warn_3");
public static readonly TranslatableString ReadOnlyWarnTitle = create("read_only_warn_title");
public static readonly TranslatableString ReadOnlyWarn = create("read_only_warn");
private static TranslatableString create(string key) => new(TranslationAreas.BaseLayout, key);
}

View file

@ -114,6 +114,9 @@ public class CommentController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
GameComment? comment = await this.DeserializeBody<GameComment>();
if (comment?.Message == null) return this.BadRequest();
@ -154,6 +157,9 @@ public class CommentController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest();
CommentEntity? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId);

View file

@ -71,7 +71,7 @@ public class LoginController : ControllerBase
switch (npTicket.Platform)
{
case Platform.RPCS3:
user = await this.database.Users.FirstOrDefaultAsync(u => u.LinkedRpcnId == npTicket.UserId);
user = await this.database.Users.FirstOrDefaultAsync(u => u.LinkedRpcnId == npTicket.UserId);
break;
case Platform.PS3:
case Platform.Vita:
@ -87,7 +87,7 @@ public class LoginController : ControllerBase
// If this user id hasn't been linked to any accounts
if (user == null)
{
// Check if there is an account with that username already
// Check if there is an account with that username already
UserEntity? targetUsername = await this.database.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
if (targetUsername != null)
{
@ -174,7 +174,7 @@ public class LoginController : ControllerBase
}
GameTokenEntity? token = await this.database.GameTokens.Include(t => t.User)
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && t.TicketHash == npTicket.TicketHash);
.FirstOrDefaultAsync(t => t.User.Username == npTicket.Username && t.TicketHash == npTicket.TicketHash);
if (token != null)
{

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
@ -37,6 +38,11 @@ public class MatchController : GameController
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();
await LastContactHelper.SetLastContact(this.database, user, token.GameVersion, token.Platform);
// Do not allow matchmaking if it has been disabled
if (!ServerConfiguration.Instance.Matchmaking.MatchmakingEnabled) return this.BadRequest();
#region Parse match data
// Example POST /match: [UpdateMyPlayerData,["Player":"FireGamer9872"]]
@ -70,15 +76,12 @@ public class MatchController : GameController
#endregion
await LastContactHelper.SetLastContact(this.database, user, token.GameVersion, token.Platform);
#region Process match data
switch (matchData)
{
case UpdateMyPlayerData playerData:
{
MatchHelper.SetUserLocation(user.UserId, token.UserLocation);
Room? room = RoomHelper.FindRoomByUser(user.UserId, token.GameVersion, token.Platform, true);
if (playerData.RoomState != null)
@ -86,19 +89,13 @@ public class MatchController : GameController
room.State = (RoomState)playerData.RoomState;
break;
}
// Check how many people are online in release builds, disabled for debug for ..well debugging.
#if DEBUG
case FindBestRoom diveInData:
#else
case FindBestRoom diveInData when MatchHelper.UserLocations.Count > 1:
#endif
{
FindBestRoomResponse? response = RoomHelper.FindBestRoom(this.database,
user,
token.GameVersion,
diveInData.RoomSlot,
token.Platform,
token.UserLocation);
token.Platform);
if (response == null) return this.NotFound();
@ -108,7 +105,7 @@ public class MatchController : GameController
return this.Ok($"[{{\"StatusCode\":200}},{serialized}]");
}
case CreateRoom createRoom when !MatchHelper.UserLocations.IsEmpty:
case CreateRoom createRoom:
{
List<int> users = new();
foreach (string playerUsername in createRoom.Players)

View file

@ -3,6 +3,8 @@ using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Localization;
using LBPUnion.ProjectLighthouse.Localization.StringLists;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Serialization;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Types;
@ -58,10 +60,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
announceText.Replace("%user", username);
announceText.Replace("%id", token.UserId.ToString());
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
{
announceText.Insert(0, BaseLayoutStrings.ReadOnlyWarn.Translate(LocalizationManager.DefaultLang) + "\n\n");
}
#if DEBUG
announceText.Append("\n\n---DEBUG INFO---\n" +
$"user.UserId: {token.UserId}\n" +
$"token.UserLocation: {token.UserLocation}\n" +
$"token.GameVersion: {token.GameVersion}\n" +
$"token.TicketHash: {token.TicketHash}\n" +
$"token.ExpiresAt: {token.ExpiresAt.ToString()}\n" +

View file

@ -32,6 +32,9 @@ public class PhotosController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
int photoCount = await this.database.Photos.CountAsync(p => p.CreatorId == token.UserId);
if (photoCount >= ServerConfiguration.Instance.UserGeneratedContentLimits.PhotosQuota) return this.BadRequest();
@ -85,7 +88,7 @@ public class PhotosController : GameController
case SlotType.Developer:
{
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId);
if (slot != null)
if (slot != null)
photoSlot.SlotId = slot.SlotId;
else
photoSlot.SlotId = await SlotHelper.GetPlaceholderSlotId(this.database, photoSlot.SlotId, photoSlot.SlotType);

View file

@ -1,4 +1,5 @@
using System.Text;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Logging;
@ -54,10 +55,14 @@ public class ResourcesController : GameController
string fullPath = Path.GetFullPath(path);
FileHelper.EnsureDirectoryCreated(assetsDirectory);
// lbp treats code 409 as success and as an indicator that the file is already present
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
// LBP treats code 409 as success and as an indicator that the file is already present
if (FileHelper.ResourceExists(hash)) return this.Conflict();
// theoretically shouldn't be possible because of hash check but handle anyways
// Theoretically shouldn't be possible because of hash check but handle anyways
if (!fullPath.StartsWith(FileHelper.FullResourcePath)) return this.BadRequest();
Logger.Info($"Processing resource upload (hash: {hash})", LogArea.Resources);

View file

@ -44,6 +44,9 @@ public class PublishController : GameController
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
GameUserSlot? slot = await this.DeserializeBody<GameUserSlot>();
if (slot == null)
{
@ -117,6 +120,9 @@ public class PublishController : GameController
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
GameUserSlot? slot = await this.DeserializeBody<GameUserSlot>();
if (slot == null)
@ -336,6 +342,9 @@ public class PublishController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == id);
if (slot == null) return this.NotFound();

View file

@ -1,3 +1,5 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
@ -87,6 +89,9 @@ public class ReviewController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
GameReview? newReview = await this.DeserializeBody<GameReview>();
if (newReview == null) return this.BadRequest();
@ -110,7 +115,7 @@ public class ReviewController : GameController
}
review.Thumb = Math.Clamp(newReview.Thumb, -1, 1);
review.LabelCollection = LabelHelper.RemoveInvalidLabels(newReview.LabelCollection);
review.Text = newReview.Text;
review.Deleted = false;
review.Timestamp = TimeHelper.TimestampMillis;
@ -148,6 +153,13 @@ public class ReviewController : GameController
List<GameReview> reviews = (await this.database.Reviews
.Where(r => r.SlotId == slotId)
.Select(r => new
{
Review = r,
SlotVersion = r.Slot!.GameVersion,
})
.Where(a => a.SlotVersion <= token.GameVersion)
.Select(a => a.Review)
.OrderByDescending(r => r.ThumbsUp - r.ThumbsDown)
.ThenByDescending(r => r.Timestamp)
.ApplyPagination(pageData)
@ -169,6 +181,13 @@ public class ReviewController : GameController
List<GameReview> reviews = (await this.database.Reviews
.Where(r => r.ReviewerId == targetUserId)
.Select(r => new
{
Review = r,
SlotVersion = r.Slot!.GameVersion,
})
.Where(a => a.SlotVersion <= token.GameVersion)
.Select(a => a.Review)
.OrderByDescending(r => r.Timestamp)
.ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token));
@ -234,6 +253,9 @@ public class ReviewController : GameController
{
GameTokenEntity token = this.GetToken();
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
int creatorId = await this.database.Slots.Where(s => s.SlotId == slotId).Select(s => s.CreatorId).FirstOrDefaultAsync();
if (creatorId == 0) return this.BadRequest();

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Files;
@ -69,6 +70,9 @@ public class UserController : GameController
if (update.Biography != null)
{
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
if (update.Biography.Length > 512) return this.BadRequest();
user.Biography = update.Biography;
@ -81,6 +85,9 @@ public class UserController : GameController
{
if (string.IsNullOrWhiteSpace(resource)) continue;
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.BadRequest();
if (!FileHelper.ResourceExists(resource) && !resource.StartsWith('g')) return this.BadRequest();
if (!GameResourceHelper.IsValidTexture(resource)) return this.BadRequest();
@ -169,7 +176,7 @@ public class UserController : GameController
// Sometimes the update gets called periodically as pin progress updates via playing,
// may not affect equipped profile pins however, so check before setting it.
string currentPins = user.Pins;
string newPins = string.Join(",", pinJson.ProfilePins);
string newPins = string.Join(",", pinJson.ProfilePins.Distinct());
if (string.Equals(currentPins, newPins)) return this.Ok("[{\"StatusCode\":200}]");

View file

@ -1,6 +1,8 @@
using System.Security.Claims;
using System.Net;
using System.Security.Claims;
using System.Text.Encodings.Web;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging.Abstractions;
@ -36,10 +38,17 @@ public class TokenAuthHandler : AuthenticationHandler<AuthenticationSchemeOption
GameTokenEntity? gameToken = await this.database.GameTokenFromRequest(this.Request);
if (gameToken == null) return AuthenticateResult.Fail("No game token");
IPAddress? remoteIpAddress = this.Context.Connection.RemoteIpAddress;
if (remoteIpAddress == null) return AuthenticateResult.Fail("Failed to determine IP address");
if (CryptoHelper.Sha256Hash(remoteIpAddress.ToString()) != gameToken.LocationHash)
return AuthenticateResult.Fail("IP address change detected");
this.Context.Items["Token"] = gameToken;
Claim[] claims = {
new("userId", gameToken.UserId.ToString()),
};
Claim[] claims =
[
new Claim("userId", gameToken.UserId.ToString()),
];
ClaimsIdentity identity = new(claims, this.Scheme.Name);
ClaimsPrincipal principal = new(identity);
AuthenticationTicket ticket = new(principal, this.Scheme.Name);

View file

@ -1,8 +1,11 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Moderation.Cases;
using LBPUnion.ProjectLighthouse.Types.Users;
@ -26,7 +29,8 @@ public class AdminUserController : ControllerBase
/// Resets the user's earth decorations to a blank state. Useful for users who abuse audio for example.
/// </summary>
[HttpGet("wipePlanets")]
public async Task<IActionResult> WipePlanets([FromRoute] int id) {
public async Task<IActionResult> WipePlanets([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
@ -90,6 +94,78 @@ public class AdminUserController : ControllerBase
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Deletes every comment by the user. Useful in case of mass spam
/// </summary>
[HttpGet("wipeComments")]
public async Task<IActionResult> WipeComments([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
// Find every comment by the user, then set the deletion info on them
await this.database.Comments.Where(c => c.PosterUserId == targetedUser.UserId)
.ExecuteUpdateAsync(s =>
s.SetProperty(c => c.Deleted, true)
.SetProperty(c => c.DeletedBy, user.Username)
.SetProperty(c => c.DeletedType, "moderator"));
Logger.Success($"Deleted comments for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin);
await this.database.SendNotification(targetedUser.UserId,
"Your comments have been deleted by a moderator.");
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Deletes every score from the user. Useful in the case where a user cheated a ton of scores
/// </summary>
[HttpGet("wipeScores")]
public async Task<IActionResult> WipeScores([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
// Find and delete every score uploaded by the target user
await this.database.Scores.Where(c => c.UserId == targetedUser.UserId).ExecuteDeleteAsync();
Logger.Success($"Deleted scores for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin);
await this.database.SendNotification(targetedUser.UserId, "Your scores have been deleted by a moderator.");
return this.Redirect($"/user/{targetedUser.UserId}");
}
/// <summary>
/// Forces the email verification of a user.
/// </summary>
[HttpGet("forceVerifyEmail")]
public async Task<IActionResult> ForceVerifyEmail([FromRoute] int id)
{
UserEntity? user = this.database.UserFromWebRequest(this.Request);
if (user == null || !user.IsModerator) return this.NotFound();
UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id);
if (targetedUser == null) return this.NotFound();
if (targetedUser.EmailAddress == null || targetedUser.EmailAddressVerified) return this.NotFound();
List<EmailVerificationTokenEntity> tokens = await this.database.EmailVerificationTokens
.Where(t => t.UserId == targetedUser.UserId)
.ToListAsync();
this.database.EmailVerificationTokens.RemoveRange(tokens);
targetedUser.EmailAddressVerified = true;
await this.database.SaveChangesAsync();
return this.Redirect($"/user/{targetedUser.UserId}");
}
[HttpPost("/admin/user/{id:int}/setPermissionLevel")]
public async Task<IActionResult> SetUserPermissionLevel([FromRoute] int id, [FromForm] PermissionLevel role)

View file

@ -1,8 +1,10 @@
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.AspNetCore.Mvc;
#if DEBUG
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
#endif
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.Debug;
@ -27,10 +29,6 @@ public class RoomVisualizerController : ControllerBase
List<int> users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(2).Select(u => u.UserId).ToListAsync();
RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3);
foreach (int user in users)
{
MatchHelper.SetUserLocation(user, "127.0.0.1");
}
return this.Redirect("/debug/roomVisualizer");
#endif
}

View file

@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
// I would like to apologize in advance for anyone dealing with this file.
// I would like to apologize in advance for anyone dealing with this file.
// Theres probably a better way to do this with delegates but I'm tired.
// TODO: Clean up this file
// - jvyden
@ -63,6 +63,9 @@ public class SlotPageController : ControllerBase
WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request);
if (token == null) return this.Redirect("~/login");
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.Redirect("~/slot/" + id);
if (msg == null)
{
Logger.Error($"Refusing to post comment from {token.UserId} on level {id}, {nameof(msg)} is null", LogArea.Comments);

View file

@ -39,6 +39,9 @@ public class UserPageController : ControllerBase
WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request);
if (token == null) return this.Redirect("~/login");
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return this.Redirect("~/user/" + id);
if (msg == null)
{
Logger.Error($"Refusing to post comment from {token.UserId} on user {id}, {nameof(msg)} is null", LogArea.Comments);

View file

@ -17,15 +17,15 @@
<script>
let shouldRefresh = true;
setTimeout(() => {
if (shouldRefresh) window.location.reload();
}, @(refreshSeconds * 1000));
function stopRefreshing() {
shouldRefresh = false;
console.log("Stopped refresh");
const stopRefreshButton = document.getElementById("stop-refresh-button");
stopRefreshButton.parentElement.removeChild(stopRefreshButton);
console.log("Removed stop refresh button");
@ -61,7 +61,7 @@
#nullable enable
if (version == GameVersion.LittleBigPlanet1 || version == GameVersion.LittleBigPlanetPSP || version == GameVersion.Unknown) continue;
FindBestRoomResponse? response = RoomHelper.FindBestRoom(Database, null, version, null, null, null);
FindBestRoomResponse? response = RoomHelper.FindBestRoom(Database, null, version, null, null);
string text = response == null ? "No room found." : "Room " + response.RoomId;
<p><b>Best room for @version.ToPrettyString()</b>: @text</p>

View file

@ -14,5 +14,4 @@
<p><b>Branch</b>: @VersionHelper.Branch</p>
<p><b>Build</b>: @VersionHelper.Build</p>
<p><b>CommitHash</b>: @VersionHelper.CommitHash</p>
<p><b>IsDirty</b>: @VersionHelper.IsDirty</p>
<p><b>RepositoryUrl</b>: @VersionHelper.RepositoryUrl</p>

View file

@ -60,7 +60,37 @@
}
}
<br><br>
@if (Model.LatestAnnouncement != null)
{
<div class="ui blue segment" style="position: relative;">
<div>
<h3>@Model.LatestAnnouncement.Title</h3>
<div style="padding-bottom: 2em;">
@if (Model.LatestAnnouncement.Content.Length > 250)
{
<span style="white-space: pre-line">@Model.LatestAnnouncement.Content[..250]...<a href="@ServerConfiguration.Instance.ExternalUrl/notifications">read more</a></span>
}
else
{
<span style="white-space: pre-line">@Model.LatestAnnouncement.Content</span>
}
</div>
@if (Model.LatestAnnouncement.Publisher != null)
{
<div class="ui tiny bottom left attached label">
Posted by
<a style="color: black" href="~/user/@Model.LatestAnnouncement.Publisher.UserId">
@Model.LatestAnnouncement.Publisher.Username
</a>
</div>
}
</div>
</div>
}
else
{
<br /><br />
}
<div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column">

View file

@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Website;
using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -19,6 +20,8 @@ public class LandingPage : BaseLayout
public int PendingAuthAttempts;
public List<UserEntity> PlayersOnline = new();
public WebsiteAnnouncementEntity? LatestAnnouncement;
public LandingPage(DatabaseContext database) : base(database)
{ }
@ -54,6 +57,10 @@ public class LandingPage : BaseLayout
.Include(s => s.Creator)
.ToListAsync();
this.LatestAnnouncement = await this.Database.WebsiteAnnouncements.Include(a => a.Publisher)
.OrderByDescending(a => a.AnnouncementId)
.FirstOrDefaultAsync();
return this.Page();
}
}

View file

@ -158,22 +158,14 @@
</div>
</div>
</noscript>
@if (!ServerStatics.IsDebug() && VersionHelper.IsDirty)
@if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
{
<div class="ui bottom attached red message large">
<div class="ui container">
<i class="warning icon"></i>
<span style="font-size: 1.2rem;">@Model.Translate(BaseLayoutStrings.LicenseWarnTitle)</span>
<span style="font-size: 1.2rem;">@Model.Translate(BaseLayoutStrings.ReadOnlyWarnTitle)</span>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1,
"<a href=\"https://github.com/LBPUnion/project-lighthouse/blob/main/LICENSE\">GNU Affero General Public License v3.0</a>"))
</p>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2,
"<code>git status</code>", "<a href=\"https://github.com/LBPUnion/project-lighthouse/issues\">", "</a>"))
</p>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn3))
@Html.Raw(Model.Translate(BaseLayoutStrings.ReadOnlyWarn))
</p>
</div>
</div>
@ -222,10 +214,6 @@
<span class="ui text red">Cannot Detect Source Code</span>
}
</span>
@if (VersionHelper.IsDirty)
{
<p>@Model.Translate(BaseLayoutStrings.GeneratedModified)</p>
}
</div>
</div>
@if (ServerStatics.IsDebug())

View file

@ -1,4 +1,5 @@
@using System.Web
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Database
@using LBPUnion.ProjectLighthouse.Localization
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@ -31,18 +32,32 @@
@if (Model.CommentsEnabled && Model.User != null)
{
<div class="ui divider"></div>
<form class="ui reply form" action="postComment" method="post">
<div class="field">
<textarea style="min-height: 70px; height: 70px; max-height:120px" maxlength="100" name="msg"></textarea>
@if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
{
<div class="ui red segment">
<p>
<i>
@ServerConfiguration.Instance.Customization.ServerName is currently in read-only mode.
You will not be able to post comments until read-only mode is disabled.
</i>
</p>
</div>
<input type="submit" class="ui blue button">
</form>
}
else
{
<form class="ui reply form" action="postComment" method="post">
<div class="field">
<textarea style="min-height: 70px; height: 70px; max-height:120px" maxlength="100" name="msg"></textarea>
</div>
<input type="submit" class="ui blue button">
</form>
}
@if (Model.Comments.Count > 0)
{
<div class="ui divider"></div>
}
}
@{
int i = 0;
foreach (KeyValuePair<CommentEntity, RatedCommentEntity?> commentAndReaction in Model.Comments)

View file

@ -4,7 +4,6 @@
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types.Entities.Level
@using LBPUnion.ProjectLighthouse.Types.Serialization
@{
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool canDelete = (bool?)ViewData["CanDelete"] ?? false;
@ -29,6 +28,18 @@
<div class="ui divider"></div>
}
@if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
{
<div class="ui red segment">
<p>
<i>
@ServerConfiguration.Instance.Customization.ServerName is currently in read-only mode.
You will not be able to post reviews in-game until read-only mode is disabled.
</i>
</p>
</div>
}
@for(int i = 0; i < Model.Reviews.Count; i++)
{
ReviewEntity review = Model.Reviews[i];
@ -36,7 +47,7 @@
-1 => review.Reviewer?.BooHash,
0 => review.Reviewer?.MehHash,
1 => review.Reviewer?.YayHash,
_ => throw new ArgumentOutOfRangeException(),
}) ?? "";
@ -49,7 +60,7 @@
-1 => "Boo!",
0 => "Meh.",
1 => "Yay!",
_ => throw new ArgumentOutOfRangeException(),
};
@ -114,7 +125,7 @@
if (window.confirm("Are you sure you want to delete this?\nThis action cannot be undone.")){
window.location.hash = "reviews";
window.location.href = "/moderation/deleteReview/" + reviewId + "?callbackUrl=" + this.window.location;
}
}
}
</script>
</div>

View file

@ -1,4 +1,5 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
@ -25,6 +26,10 @@ public class SlotSettingsPage : BaseLayout
if (!this.User.IsModerator && this.User != this.Slot.Creator) return this.Redirect("~/slot/" + slotId);
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
return this.Redirect($"~/slot/{slotId}");
string? avatarHash = await FileHelper.ParseBase64Image(avatar);
if (avatarHash != null) this.Slot.IconHash = avatarHash;
@ -46,7 +51,7 @@ public class SlotSettingsPage : BaseLayout
if (labels != null)
{
labels = LabelHelper.RemoveInvalidLabels(labels);
if (this.Slot.AuthorLabels != labels)
if (this.Slot.AuthorLabels != labels)
this.Slot.AuthorLabels = labels;
}

View file

@ -324,6 +324,18 @@ else
<i class="trash alternate icon"></i>
<span>Wipe Earth Decorations</span>
</a>
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipeComments"
onclick="return confirm('Are you sure you want to delete ALL of this user\'s comments? This action cannot be reversed.')">
<i class="trash alternate icon"></i>
<span>Wipe User&apos;s Comments</span>
</a>
<a class="ui red button" href="/moderation/user/@Model.ProfileUser.UserId/wipeScores"
onclick="return confirm('Are you sure you want to delete ALL of this user\'s scores? This action cannot be reversed.')">
<i class="trash alternate icon"></i>
<span>Wipe User&apos;s Scores</span>
</a>
@if (!Model.CommentsDisabledByModerator)
{
@ -333,6 +345,14 @@ else
</a>
}
@if (Model.ProfileUser.EmailAddress != null && !Model.ProfileUser.EmailAddressVerified)
{
<a class="ui green button" href="/moderation/user/@Model.ProfileUser.UserId/forceVerifyEmail">
<i class="check icon"></i>
<span>Forcibly Verify Email</span>
</a>
}
@if (Model.User.IsAdmin)
{
<div class="ui divider"></div>

View file

@ -45,7 +45,8 @@ public class UserPage : BaseLayout
if (this.ProfileUser == null) return this.NotFound();
string userSlug = this.ProfileUser.GenerateSlug();
if (slug == null || userSlug != slug)
// Only redirect if there is a valid slug for this user and the current slug doesn't match
if (!string.IsNullOrWhiteSpace(userSlug) && (slug == null || userSlug != slug))
{
return this.Redirect($"~/user/{userId}/{userSlug}");
}

View file

@ -39,6 +39,10 @@ public class UserSettingsPage : BaseLayout
if (!this.User.IsModerator && this.User != this.ProfileUser) return this.Redirect("~/user/" + userId);
// Deny request if in read-only mode
if (avatar != null && ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
return this.Redirect($"~/user/{userId}");
string? avatarHash = await FileHelper.ParseBase64Image(avatar);
if (avatarHash != null) this.ProfileUser.IconHash = avatarHash;
@ -47,6 +51,10 @@ public class UserSettingsPage : BaseLayout
if (biography != null)
{
// Deny request if in read-only mode
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
return this.Redirect($"~/user/{userId}");
biography = CensorHelper.FilterMessage(biography);
if (this.ProfileUser.Biography != biography && biography.Length <= 512)
this.ProfileUser.Biography = biography;

View file

@ -11,6 +11,6 @@
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Localization\ProjectLighthouse.Localization.csproj" />
<ProjectReference Include="..\ProjectLighthouse\ProjectLighthouse.csproj" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="QRCoder" Version="1.5.1" />
</ItemGroup>
</Project>

View file

@ -20,8 +20,8 @@ public class DatabaseTests : LighthouseServerTest<GameServerTestStartup>
int rand = new Random().Next();
UserEntity userA = await database.CreateUser("unitTestUser" + rand, CryptoHelper.GenerateAuthToken());
UserEntity userB = await database.CreateUser("unitTestUser" + rand, CryptoHelper.GenerateAuthToken());
UserEntity userA = await database.CreateUser("unitTestUser" + rand, CryptoHelper.BCryptHash(CryptoHelper.GenerateAuthToken()));
UserEntity userB = await database.CreateUser("unitTestUser" + rand, CryptoHelper.BCryptHash(CryptoHelper.GenerateAuthToken()));
Assert.NotNull(userA);
Assert.NotNull(userB);

View file

@ -9,18 +9,18 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers;
[Trait("Category", "Unit")]
public class ReviewControllerTests
{
private static async Task InsertTestData(DatabaseContext database)
{
database.Slots.Add(new SlotEntity
{
SlotId = 1,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet3,
});
database.Slots.Add(new SlotEntity
{
SlotId = 2,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet2,
});
database.Reviews.Add(new ReviewEntity
{
ReviewId = 1,
ReviewerId = 1,
SlotId = 1,
});
database.Reviews.Add(new ReviewEntity
{
ReviewId = 2,
ReviewerId = 1,
SlotId = 2,
});
await database.SaveChangesAsync();
}
[Theory]
[InlineData(GameVersion.LittleBigPlanet2, 1)]
[InlineData(GameVersion.LittleBigPlanet3, 2)]
public async Task ReviewsBy_ShouldNotList_HigherGameVersions(GameVersion version, int expected)
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = version;
DatabaseContext database = await MockHelper.GetTestDatabase(new List<GameTokenEntity>
{
token,
});
await InsertTestData(database);
ReviewController controller = new(database);
controller.SetupTestController(token);
IActionResult response = await controller.ReviewsBy("unittest");
ReviewResponse review = response.CastTo<OkObjectResult, ReviewResponse>();
Assert.Equal(expected, review.Reviews.Count);
Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version));
}
[Theory]
[InlineData(GameVersion.LittleBigPlanet2, 2, 1)]
[InlineData(GameVersion.LittleBigPlanet2, 1, 0)]
[InlineData(GameVersion.LittleBigPlanet3, 2, 1)]
[InlineData(GameVersion.LittleBigPlanet3, 1, 1)]
public async Task ReviewsFor_ShouldNotList_HigherGameVersions(GameVersion version, int slotId, int expected)
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = version;
DatabaseContext database = await MockHelper.GetTestDatabase(new List<GameTokenEntity>
{
token,
});
await InsertTestData(database);
ReviewController controller = new(database);
controller.SetupTestController(token);
IActionResult response = await controller.ReviewsFor(slotId);
ReviewResponse review = response.CastTo<OkObjectResult, ReviewResponse>();
Assert.Equal(expected, review.Reviews.Count);
Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version));
}
}

View file

@ -177,4 +177,29 @@ public class UserControllerTests
Assert.Equal(expectedPins, dbMock.Users.First().Pins);
Assert.Equal(expectedResponse, pinsResponse);
}
[Fact]
public async Task UpdateMyPins_ShouldRemove_DuplicatePins()
{
UserEntity entity = MockHelper.GetUnitTestUser();
entity.Pins = "1234";
List<UserEntity> users = new()
{
entity,
};
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users);
UserController userController = new(dbMock);
userController.SetupTestController("{\"profile_pins\": [1234, 1234]}");
const string expectedPins = "1234";
const string expectedResponse = "[{\"StatusCode\":200}]";
IActionResult result = await userController.UpdateMyPins();
string pinsResponse = result.CastTo<OkObjectResult, string>();
Assert.Equal(expectedPins, dbMock.Users.First().Pins);
Assert.Equal(expectedResponse, pinsResponse);
}
}

View file

@ -9,20 +9,20 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.17.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.21.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="125.0.6422.14100" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -37,7 +37,6 @@ public static class MockHelper
UserId = 1,
ExpiresAt = DateTime.MaxValue,
TokenId = 1,
UserLocation = "127.0.0.1",
UserToken = "unittest",
};

View file

@ -14,23 +14,23 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
</ItemGroup>
<ItemGroup>

View file

@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/Asp/FormatOnClosingTag/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/CSharpAnnotationTypingAssist/IsEnabledAfterTypeName/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeEditing/TypingAssist/CSharpAnnotationTypingAssist/IsEnabledAtOtherPositions/@EntryValue">False</s:Boolean>
@ -96,10 +98,14 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Method/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8284009d_002De743_002D4d89_002D9402_002Da5bf9a89b657/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"&gt;&lt;ElementKinds&gt;&lt;Kind Name="METHOD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@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/=ARRRRRRRRRR/@EntryIndexedValue">True</s:Boolean>

View file

@ -0,0 +1,6 @@
namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
public class MatchmakingConfiguration
{
public bool MatchmakingEnabled { get; set; } = true;
}

View file

@ -11,6 +11,12 @@ public class UserGeneratedContentLimitConfiguration
public int PhotosQuota { get; set; } = 500;
/// <summary>
/// When enabled, all UGC uploads are disabled. This includes levels, photos, reviews,
/// comments, and certain profile settings.
/// </summary>
public bool ReadOnlyMode { get; set; } = false;
public bool ProfileCommentsEnabled { get; set; } = true;
public bool LevelCommentsEnabled { get; set; } = true;

View file

@ -11,7 +11,7 @@ public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
// This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly.
// If you are modifying anything here, this value MUST be incremented.
// Thanks for listening~
public override int ConfigVersion { get; set; } = 24;
public override int ConfigVersion { get; set; } = 26;
public override string ConfigName { get; set; } = "lighthouse.yml";
public string WebsiteListenUrl { get; set; } = "http://localhost:10060";
@ -35,6 +35,7 @@ public class ServerConfiguration : ConfigurationBase<ServerConfiguration>
public AuthenticationConfiguration Authentication { get; set; } = new();
public CaptchaConfiguration Captcha { get; set; } = new();
public DigestKeyConfiguration DigestKey { get; set; } = new();
public MatchmakingConfiguration Matchmaking { get; set; } = new();
public GoogleAnalyticsConfiguration GoogleAnalytics { get; set; } = new();
public MailConfiguration Mail { get; set; } = new();
public UserGeneratedContentLimitConfiguration UserGeneratedContentLimits { get; set; } = new();

View file

@ -67,10 +67,10 @@ public partial class DatabaseContext
UserToken = CryptoHelper.GenerateAuthToken(),
User = user,
UserId = user.UserId,
UserLocation = userLocation,
GameVersion = npTicket.GameVersion,
Platform = npTicket.Platform,
TicketHash = npTicket.TicketHash,
LocationHash = CryptoHelper.Sha256Hash(userLocation),
// we can get away with a low expiry here since LBP will just get a new token everytime it gets 403'd
ExpiresAt = DateTime.UtcNow + TimeSpan.FromHours(1),
};

View file

@ -9,9 +9,6 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static class CryptoHelper
{
private static readonly SHA256 sha256 = SHA256.Create();
/// <summary>
/// Generates a random SHA256 and BCrypted token
/// </summary>
@ -19,10 +16,15 @@ public static class CryptoHelper
public static string GenerateAuthToken()
{
byte[] bytes = (byte[])GenerateRandomBytes(256);
return BCryptHash(Sha256Hash(bytes));
}
public static string GenerateUrlToken()
{
byte[] bytes = (byte[])GenerateRandomBytes(256);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(BCryptHash(Sha256Hash(bytes))));
}
public static string ComputeDigest(string path, string authCookie, byte[] body, string digestKey, bool excludeBody = false)
{
@ -156,7 +158,7 @@ public static class CryptoHelper
public static string Sha256Hash(string str) => Sha256Hash(Encoding.UTF8.GetBytes(str));
public static string Sha256Hash(byte[] bytes) => BitConverter.ToString(sha256.ComputeHash(bytes)).Replace("-", "").ToLower();
public static string Sha256Hash(byte[] bytes) => BitConverter.ToString(SHA256.HashData(bytes)).Replace("-", "").ToLower();
public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(SHA1.HashData(bytes)).Replace("-", "");

View file

@ -52,7 +52,7 @@ public static class SMTPHelper
{
Created = DateTime.UtcNow,
UserId = user.UserId,
ResetToken = CryptoHelper.GenerateAuthToken(),
ResetToken = CryptoHelper.GenerateUrlToken(),
};
database.PasswordResetTokens.Add(token);
@ -92,7 +92,7 @@ public static class SMTPHelper
{
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
EmailToken = CryptoHelper.GenerateUrlToken(),
ExpiresAt = DateTime.UtcNow.AddHours(6),
};

View file

@ -11,15 +11,8 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static partial class MatchHelper
{
public static readonly ConcurrentDictionary<int, string?> UserLocations = new();
public static readonly ConcurrentDictionary<int, List<int>?> UserRecentlyDivedIn = new();
public static void SetUserLocation(int userId, string location)
{
if (UserLocations.TryGetValue(userId, out string? _)) UserLocations.TryRemove(userId, out _);
UserLocations.TryAdd(userId, location);
}
public static void AddUserRecentlyDivedIn(int userId, int otherUserId)
{
if (!UserRecentlyDivedIn.TryGetValue(userId, out List<int>? recentlyDivedIn)) UserRecentlyDivedIn.TryAdd(userId, recentlyDivedIn = new List<int>());

View file

@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
@ -16,6 +15,9 @@ using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Matchmaking;
using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms;
using LBPUnion.ProjectLighthouse.Types.Users;
#if DEBUG
using System.Diagnostics;
#endif
namespace LBPUnion.ProjectLighthouse.Helpers;
@ -27,7 +29,7 @@ public static class RoomHelper
private static int roomIdIncrement;
internal static int RoomIdIncrement => roomIdIncrement++;
public static FindBestRoomResponse? FindBestRoom(DatabaseContext database, UserEntity? user, GameVersion roomVersion, RoomSlot? slot, Platform? platform, string? location)
public static FindBestRoomResponse? FindBestRoom(DatabaseContext database, UserEntity? user, GameVersion roomVersion, RoomSlot? slot, Platform? platform)
{
if (roomVersion == GameVersion.LittleBigPlanet1 || roomVersion == GameVersion.LittleBigPlanetPSP)
{
@ -61,30 +63,10 @@ public static class RoomHelper
{
if (user != null && MatchHelper.DidUserRecentlyDiveInWith(user.UserId, room.HostId)) continue;
Dictionary<int, string> relevantUserLocations = new();
// Determine if all players in a room have UserLocations stored, also store the relevant userlocations while we're at it
bool allPlayersHaveLocations = room.PlayerIds.All
(
p =>
{
bool gotValue = MatchHelper.UserLocations.TryGetValue(p, out string? value);
if (gotValue && value != null) relevantUserLocations.TryAdd(p, value);
return gotValue;
}
);
// If we don't have all locations then the game won't know how to communicate. Thus, it's not a valid room.
if (!allPlayersHaveLocations) continue;
// If we got here then it should be a valid room.
FindBestRoomResponse response = new()
{
RoomId = room.RoomId,
Players = new List<Player>(),
Locations = new List<string>(),
};
foreach (UserEntity player in room.GetPlayers(database))
@ -97,8 +79,6 @@ public static class RoomHelper
User = player,
}
);
response.Locations.Add(relevantUserLocations.GetValueOrDefault(player.UserId)); // Already validated to exist
}
if (user != null)
@ -111,8 +91,6 @@ public static class RoomHelper
}
);
if (location == null) response.Locations.Add(location);
response.Slots = new List<List<int>>
{
new()
@ -189,7 +167,7 @@ public static class RoomHelper
{
StorableList<Room> rooms = Rooms; // cache rooms so we dont gen a new one every time
List<Room> roomsToUpdate = new();
#if DEBUG
Logger.Debug($"Cleaning up rooms... (took {stopwatch.ElapsedMilliseconds}ms to get lock on {nameof(RoomLock)})", LogArea.Match);
#endif
@ -205,7 +183,7 @@ public static class RoomHelper
.ToList();
foreach (int player in playersToRemove) room.PlayerIds.Remove(player);
roomsToUpdate.Add(room);
}
@ -248,7 +226,7 @@ public static class RoomHelper
int roomCountAfterCleanup = rooms.Count();
// Log the amount of rooms cleaned up.
// If we didnt clean any rooms, it's not useful to log in a
// If we didnt clean any rooms, it's not useful to log in a
// production environment but it's still quite useful for debugging.
//
// So, we handle that case here:

View file

@ -19,7 +19,6 @@ public static class VersionHelper
public static string EnvVer => $"{ServerConfiguration.Instance.Customization.EnvironmentName} {FullRevision}";
public static string FullVersion =>
$"Project Lighthouse {ServerConfiguration.Instance.Customization.EnvironmentName} {Branch}@{CommitHash} {Build}";
public static bool IsDirty => ThisAssembly.Git.IsDirty;
public static string RepositoryUrl => ThisAssembly.Git.RepositoryUrl;
public const string Build =

View file

@ -0,0 +1,30 @@
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LBPUnion.ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20240317231145_RemoveUserLocationFromGameTokens")]
public partial class RemoveUserLocationFromGameTokens : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "UserLocation",
table: "GameTokens");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UserLocation",
table: "GameTokens",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

View file

@ -0,0 +1,598 @@
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LBPUnion.ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20240625004457_AddLocationHashToGameToken")]
public partial class AddLocationHashToGameToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "WebTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "AnnouncementId",
table: "WebsiteAnnouncements",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "VisitedLevelId",
table: "VisitedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "Users",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "SlotId",
table: "Slots",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ScoreId",
table: "Scores",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReviewId",
table: "Reviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReportId",
table: "Reports",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "RegistrationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedReviewId",
table: "RatedReviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedLevelId",
table: "RatedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatingId",
table: "RatedComments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "QueuedLevelId",
table: "QueuedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlaylistId",
table: "Playlists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlatformLinkAttemptId",
table: "PlatformLinkAttempts",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoSubjectId",
table: "PhotoSubjects",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoId",
table: "Photos",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "PasswordResetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notifications",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedProfileId",
table: "HeartedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedPlaylistId",
table: "HeartedPlaylists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedLevelId",
table: "HeartedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<string>(
name: "TicketHash",
table: "GameTokens",
type: "varchar(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "GameTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AddColumn<string>(
name: "LocationHash",
table: "GameTokens",
type: "varchar(64)",
maxLength: 64,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<int>(
name: "EmailVerificationTokenId",
table: "EmailVerificationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailSetTokenId",
table: "EmailSetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "CustomCategories",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CommentId",
table: "Comments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CaseId",
table: "Cases",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "BlockedProfileId",
table: "BlockedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "APIKeys",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LocationHash",
table: "GameTokens");
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "WebTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "AnnouncementId",
table: "WebsiteAnnouncements",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "VisitedLevelId",
table: "VisitedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "Users",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "SlotId",
table: "Slots",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ScoreId",
table: "Scores",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReviewId",
table: "Reviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReportId",
table: "Reports",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "RegistrationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedReviewId",
table: "RatedReviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedLevelId",
table: "RatedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatingId",
table: "RatedComments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "QueuedLevelId",
table: "QueuedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlaylistId",
table: "Playlists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlatformLinkAttemptId",
table: "PlatformLinkAttempts",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoSubjectId",
table: "PhotoSubjects",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoId",
table: "Photos",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "PasswordResetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notifications",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedProfileId",
table: "HeartedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedPlaylistId",
table: "HeartedPlaylists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedLevelId",
table: "HeartedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<string>(
name: "TicketHash",
table: "GameTokens",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(64)",
oldMaxLength: 64,
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "GameTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailVerificationTokenId",
table: "EmailVerificationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailSetTokenId",
table: "EmailSetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "CustomCategories",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CommentId",
table: "Comments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CaseId",
table: "Cases",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "BlockedProfileId",
table: "BlockedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "APIKeys",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
}
}
}

View file

@ -3,6 +3,7 @@ using System;
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
@ -16,15 +17,19 @@ namespace ProjectLighthouse.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.13")
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b =>
{
b.Property<int>("HeartedLevelId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("HeartedLevelId"));
b.Property<int>("SlotId")
.HasColumnType("int");
@ -46,6 +51,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("HeartedPlaylistId"));
b.Property<int>("PlaylistId")
.HasColumnType("int");
@ -67,6 +74,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("HeartedProfileId"));
b.Property<int>("HeartedUserId")
.HasColumnType("int");
@ -88,6 +97,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("QueuedLevelId"));
b.Property<int>("SlotId")
.HasColumnType("int");
@ -109,6 +120,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("RatingId"));
b.Property<int>("CommentId")
.HasColumnType("int");
@ -133,6 +146,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("RatedLevelId"));
b.Property<int>("Rating")
.HasColumnType("int");
@ -163,6 +178,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("RatedReviewId"));
b.Property<int>("ReviewId")
.HasColumnType("int");
@ -187,6 +204,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("VisitedLevelId"));
b.Property<int>("PlaysLBP1")
.HasColumnType("int");
@ -217,6 +236,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("CategoryId"));
b.Property<string>("Description")
.HasColumnType("longtext");
@ -243,6 +264,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("PlaylistId"));
b.Property<int>("CreatorId")
.HasColumnType("int");
@ -271,6 +294,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ReviewId"));
b.Property<bool>("Deleted")
.HasColumnType("tinyint(1)");
@ -318,6 +343,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ScoreId"));
b.Property<int>("ChildSlotId")
.HasColumnType("int");
@ -351,6 +378,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("SlotId"));
b.Property<string>("AuthorLabels")
.IsRequired()
.HasColumnType("longtext");
@ -504,6 +533,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("ReportId"));
b.Property<string>("Bounds")
.HasColumnType("longtext");
@ -550,6 +581,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("CaseId"));
b.Property<int>("AffectedId")
.HasColumnType("int");
@ -604,6 +637,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsDismissed")
.HasColumnType("tinyint(1)");
@ -629,6 +664,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("BlockedProfileId"));
b.Property<int>("BlockedUserId")
.HasColumnType("int");
@ -650,6 +687,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("CommentId"));
b.Property<bool>("Deleted")
.HasColumnType("tinyint(1)");
@ -719,6 +758,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("PhotoId"));
b.Property<int>("CreatorId")
.HasColumnType("int");
@ -759,6 +800,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("PhotoSubjectId"));
b.Property<string>("Bounds")
.HasColumnType("longtext");
@ -783,6 +826,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("PlatformLinkAttemptId"));
b.Property<string>("IPAddress")
.HasColumnType("longtext");
@ -811,6 +856,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("UserId"));
b.Property<int>("AdminGrantedSlots")
.HasColumnType("int");
@ -916,6 +963,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
@ -936,6 +985,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("EmailSetTokenId"));
b.Property<string>("EmailToken")
.HasColumnType("longtext");
@ -958,6 +1009,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("EmailVerificationTokenId"));
b.Property<string>("EmailToken")
.HasColumnType("longtext");
@ -980,24 +1033,28 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("TokenId"));
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)");
b.Property<int>("GameVersion")
.HasColumnType("int");
b.Property<string>("LocationHash")
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<int>("Platform")
.HasColumnType("int");
b.Property<string>("TicketHash")
.HasColumnType("longtext");
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<int>("UserId")
.HasColumnType("int");
b.Property<string>("UserLocation")
.HasColumnType("longtext");
b.Property<string>("UserToken")
.HasColumnType("longtext");
@ -1014,6 +1071,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("TokenId"));
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
@ -1034,6 +1093,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("TokenId"));
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
@ -1054,6 +1115,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("TokenId"));
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)");
@ -1077,6 +1140,8 @@ namespace ProjectLighthouse.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("AnnouncementId"));
b.Property<string>("Content")
.HasColumnType("longtext");

View file

@ -10,31 +10,31 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="GitInfo" Version="3.3.3">
<PackageReference Include="GitInfo" Version="3.3.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pfim" Version="0.11.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="Discord.Net.Webhook" Version="3.13.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="Discord.Net.Webhook" Version="3.15.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
<PackageReference Include="Redis.OM" Version="0.6.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Redis.OM" Version="0.7.1" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="YamlDotNet" Version="15.1.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.2.1" />
<PackageReference Include="DistributedLock.MySql" Version="1.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="YamlDotNet" Version="15.1.6" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="DistributedLock.MySql" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ProjectLighthouse.Localization\ProjectLighthouse.Localization.csproj" />
</ItemGroup>
</Project>

View file

@ -53,12 +53,6 @@ public static class StartupTasks
// Version info depends on ServerConfig
Logger.Info($"You are running version {VersionHelper.FullVersion}", LogArea.Startup);
if (VersionHelper.IsDirty)
{
Logger.Warn("This is a modified version of Project Lighthouse. " +
"Please make sure you are properly disclosing the source code to any users who may be using this instance.",
LogArea.Startup);
}
Logger.Info("Connecting to the database...", LogArea.Startup);

View file

@ -185,7 +185,7 @@ public class NPTicket
}
// Used to identify duplicate tickets
npTicket.TicketHash = CryptoHelper.Sha1Hash(data);
npTicket.TicketHash = CryptoHelper.Sha256Hash(data);
#if DEBUG
Logger.Debug("npTicket data:", LogArea.Login);

View file

@ -33,6 +33,9 @@ public struct SectionHeader
public SectionType Type;
public ushort Length;
public int Position;
public override string ToString() =>
$"SectionHeader(Type='{this.Type}', Length='{this.Length}', Position='{this.Position}')";
}
public class TicketReader : BinaryReader

View file

@ -19,13 +19,15 @@ public class GameTokenEntity
public string UserToken { get; set; }
public string UserLocation { get; set; }
public GameVersion GameVersion { get; set; }
public Platform Platform { get; set; }
[StringLength(64)]
public string TicketHash { get; set; }
[StringLength(64)]
public string LocationHash { get; set; }
public DateTime ExpiresAt { get; set; }
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms;
@ -14,8 +13,4 @@ public class FindBestRoomResponse
[JsonIgnore]
public IEnumerable<int> FirstSlot => this.Slots[0];
[JsonPropertyName("Location")]
[SuppressMessage("ReSharper", "CollectionNeverQueried.Global")]
public List<string> Locations { get; set; }
}

View file

@ -1,6 +1,8 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
@ -30,6 +32,13 @@ public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization
[XmlElement("photoCount")]
public int PhotoCount { get; set; }
[XmlElement("commentsEnabled")]
public bool CommentsEnabled
{
get => ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled;
set => throw new NotSupportedException();
}
public async Task PrepareSerialization(DatabaseContext database)
{
if (this.SlotId == 0 || this.InternalSlotId == 0) return;