Merge branch 'main' into crowdin

This commit is contained in:
jvyden 2022-07-26 16:15:53 -04:00
commit bac1cad9f2
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
73 changed files with 1091 additions and 272 deletions

View file

@ -7,6 +7,12 @@
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]
},
"dotnet-trace": {
"version": "6.0.328102",
"commands": [
"dotnet-trace"
]
} }
} }
} }

View file

@ -4,6 +4,9 @@
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />

View file

@ -4,6 +4,9 @@
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />

View file

@ -4,6 +4,9 @@
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/ProjectLighthouse" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" /> <option name="RUNTIME_ARGUMENTS" value="" />

View file

@ -17,9 +17,6 @@ Once you've gotten MySQL running you can run Lighthouse. It will take care of th
PS3 is difficult to set up, so I will be going over how to set up RPCS3 instead. A guide will be coming for PS3 closer PS3 is difficult to set up, so I will be going over how to set up RPCS3 instead. A guide will be coming for PS3 closer
to release. You can also follow this guide if you want to learn how to modify your EBOOT. to release. You can also follow this guide if you want to learn how to modify your EBOOT.
*Note: This requires a patched copy of RPCS3. You can find a working
version [on our GitHub](https://github.com/LBPUnion/rpcs3).*
Start by getting a copy of LittleBigPlanet 1/2 installed. (Check the LittleBigPlanet 1 section, since you'll need to do Start by getting a copy of LittleBigPlanet 1/2 installed. (Check the LittleBigPlanet 1 section, since you'll need to do
extra steps for your game to not crash upon entering pod computer). extra steps for your game to not crash upon entering pod computer).
@ -44,8 +41,8 @@ Now, copy the `EBOOTlocalhost.elf` file to where you got your `EBOOT.elf` file f
To launch the game with the patched EBOOT, open up RPCS3, go to File, Boot SELF/ELF, and open up `EBOOTlocalhost.elf`. To launch the game with the patched EBOOT, open up RPCS3, go to File, Boot SELF/ELF, and open up `EBOOTlocalhost.elf`.
Assuming you are running the patched version of RPCS3, you patched the file correctly, the database is migrated, and Assuming you patched the file correctly, the database is migrated, and
Project Lighthouse is running, the game should now connect and you may begin contributing! Project Lighthouse is running, the game should now connect, and you may begin contributing!
### LittleBigPlanet 1 ### LittleBigPlanet 1

View file

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -54,4 +55,33 @@ public class UserEndpoints : ApiEndpointController
return this.Ok(userStatus); return this.Ok(userStatus);
} }
[HttpPost("user/inviteToken")]
public async Task<IActionResult> CreateUserInviteToken()
{
if (Configuration.ServerConfiguration.Instance.Authentication.PrivateRegistration ||
Configuration.ServerConfiguration.Instance.Authentication.RegistrationEnabled)
{
string authHeader = this.Request.Headers["Authorization"];
if (!string.IsNullOrWhiteSpace(authHeader))
{
string authToken = authHeader.Substring(authHeader.IndexOf(' ') + 1);
APIKey? apiKey = await this.database.APIKeys.FirstOrDefaultAsync(k => k.Key == authToken);
if (apiKey == null) return this.StatusCode(403, null);
RegistrationToken token = new();
token.Created = DateTime.Now;
token.Token = CryptoHelper.GenerateAuthToken();
this.database.RegistrationTokens.Add(token);
await this.database.SaveChangesAsync();
return Ok(token.Token);
}
}
return this.NotFound();
}
} }

View file

@ -29,7 +29,7 @@ public class ClientConfigurationController : ControllerBase
HostString hostname = this.Request.Host; HostString hostname = this.Request.Host;
return this.Ok return this.Ok
( (
"ProbabilityOfPacketDelay 0.0\nMinPacketDelayFrames 0\nMaxPacketDelayFrames 3\nProbabilityOfPacketDrop 0.0\nEnableFakeConditionsForLoopback true\nNumberOfFramesPredictionAllowedForNonLocalPlayer 1000\nEnablePrediction true\nMinPredictedFrames 0\nMaxPredictedFrames 10\nAllowGameRendCameraSplit true\nFramesBeforeAgressiveCatchup 30\nPredictionPadSides 200\nPredictionPadTop 200\nPredictionPadBottom 200\nShowErrorNumbers true\nAllowModeratedLevels false\nAllowModeratedPoppetItems false\nTIMEOUT_WAIT_FOR_JOIN_RESPONSE_FROM_PREV_PARTY_HOST 50.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_HOST 30.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_MEMBER 45.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN_FRIEND 15.0\nTIMEOUT_WAIT_FOR_CONNECTION_FROM_HOST 30.0\nTIMEOUT_WAIT_FOR_ROOM_ID_TO_JOIN 60.0\nTIMEOUT_WAIT_FOR_GET_NUM_PLAYERS_ONLINE 60.0\nTIMEOUT_WAIT_FOR_SIGNALLING_CONNECTIONS 120.0\nTIMEOUT_WAIT_FOR_PARTY_DATA 60.0\nTIME_TO_WAIT_FOR_LEAVE_MESSAGE_TO_COME_BACK 20.0\nTIME_TO_WAIT_FOR_FOLLOWING_REQUESTS_TO_ARRIVE 30.0\nTIMEOUT_WAIT_FOR_FINISHED_MIGRATING_HOST 30.0\nTIMEOUT_WAIT_FOR_PARTY_LEADER_FINISH_JOINING 45.0\nTIMEOUT_WAIT_FOR_QUICKPLAY_LEVEL 60.0\nTIMEOUT_WAIT_FOR_PLAYERS_TO_JOIN 30.0\nTIMEOUT_WAIT_FOR_DIVE_IN_PLAYERS 240.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 60.0\nTIMEOUT_DIVE_IN_TOTAL 300.0\nTIMEOUT_WAIT_FOR_SOCKET_CONNECTION 120.0\nTIMEOUT_WAIT_FOR_REQUEST_RESOURCE_MESSAGE 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_GET_RESOURCE_LIST 120.0\nTIMEOUT_WAIT_FOR_CLIENT_TO_LOAD_RESOURCES 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_SAVE_GAME_STATE 30.0\nTIMEOUT_WAIT_FOR_ADD_PLAYERS_TO_TAKE 30.0\nTIMEOUT_WAIT_FOR_UPDATE_FROM_CLIENT 90.0\nTIMEOUT_WAIT_FOR_HOST_TO_GET_RESOURCE_LIST 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_SAVE_GAME_STATE 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_ADD_US 30.0\nTIMEOUT_WAIT_FOR_UPDATE 60.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN 50.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_PRESENCE 60.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_CONNECTION 120.0\nSECONDS_BETWEEN_PINS_AWARDED_UPLOADS 300.0\nEnableKeepAlive true\nAllowVoIPRecordingPlayback true\nOverheatingThresholdDisallowMidgameJoin 0.95\nMaxCatchupFrames 3\nMaxLagBeforeShowLoading 23\nMinLagBeforeHideLoading 30\nLagImprovementInflectionPoint -1.0\nFlickerThreshold 2.0\nClosedDemo2014Version 1\nClosedDemo2014Expired false\nEnablePlayedFilter true\nEnableCommunityDecorations true\nGameStateUpdateRate 10.0\nGameStateUpdateRateWithConsumers 1.0\nDisableDLCPublishCheck false\nEnableDiveIn true\nEnableHackChecks false\nAllowOnlineCreate true" + "ProbabilityOfPacketDelay 0.0\nMinPacketDelayFrames 0\nMaxPacketDelayFrames 3\nProbabilityOfPacketDrop 0.0\nEnableFakeConditionsForLoopback true\nNumberOfFramesPredictionAllowedForNonLocalPlayer 1000\nEnablePrediction true\nMinPredictedFrames 0\nMaxPredictedFrames 10\nAllowGameRendCameraSplit true\nFramesBeforeAgressiveCatchup 30\nPredictionPadSides 200\nPredictionPadTop 200\nPredictionPadBottom 200\nShowErrorNumbers true\nAllowModeratedLevels false\nAllowModeratedPoppetItems false\nTIMEOUT_WAIT_FOR_JOIN_RESPONSE_FROM_PREV_PARTY_HOST 50.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_HOST 30.0\nTIMEOUT_WAIT_FOR_CHANGE_LEVEL_PARTY_MEMBER 45.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN_FRIEND 15.0\nTIMEOUT_WAIT_FOR_CONNECTION_FROM_HOST 30.0\nTIMEOUT_WAIT_FOR_ROOM_ID_TO_JOIN 60.0\nTIMEOUT_WAIT_FOR_GET_NUM_PLAYERS_ONLINE 60.0\nTIMEOUT_WAIT_FOR_SIGNALLING_CONNECTIONS 120.0\nTIMEOUT_WAIT_FOR_PARTY_DATA 60.0\nTIME_TO_WAIT_FOR_LEAVE_MESSAGE_TO_COME_BACK 20.0\nTIME_TO_WAIT_FOR_FOLLOWING_REQUESTS_TO_ARRIVE 30.0\nTIMEOUT_WAIT_FOR_FINISHED_MIGRATING_HOST 30.0\nTIMEOUT_WAIT_FOR_PARTY_LEADER_FINISH_JOINING 45.0\nTIMEOUT_WAIT_FOR_QUICKPLAY_LEVEL 60.0\nTIMEOUT_WAIT_FOR_PLAYERS_TO_JOIN 30.0\nTIMEOUT_WAIT_FOR_DIVE_IN_PLAYERS 240.0\nTIMEOUT_WAIT_FOR_FIND_BEST_ROOM 60.0\nTIMEOUT_DIVE_IN_TOTAL 300.0\nTIMEOUT_WAIT_FOR_SOCKET_CONNECTION 120.0\nTIMEOUT_WAIT_FOR_REQUEST_RESOURCE_MESSAGE 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_GET_RESOURCE_LIST 120.0\nTIMEOUT_WAIT_FOR_CLIENT_TO_LOAD_RESOURCES 120.0\nTIMEOUT_WAIT_FOR_LOCAL_CLIENT_TO_SAVE_GAME_STATE 30.0\nTIMEOUT_WAIT_FOR_ADD_PLAYERS_TO_TAKE 30.0\nTIMEOUT_WAIT_FOR_UPDATE_FROM_CLIENT 90.0\nTIMEOUT_WAIT_FOR_HOST_TO_GET_RESOURCE_LIST 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_SAVE_GAME_STATE 60.0\nTIMEOUT_WAIT_FOR_HOST_TO_ADD_US 30.0\nTIMEOUT_WAIT_FOR_UPDATE 60.0\nTIMEOUT_WAIT_FOR_REQUEST_JOIN 50.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_PRESENCE 60.0\nTIMEOUT_WAIT_FOR_AUTOJOIN_CONNECTION 120.0\nSECONDS_BETWEEN_PINS_AWARDED_UPLOADS 300.0\nEnableKeepAlive true\nAllowVoIPRecordingPlayback true\nOverheatingThresholdDisallowMidgameJoin 0.95\nMaxCatchupFrames 3\nMaxLagBeforeShowLoading 23\nMinLagBeforeHideLoading 30\nLagImprovementInflectionPoint -1.0\nFlickerThreshold 2.0\nClosedDemo2014Version 1\nClosedDemo2014Expired false\nEnablePlayedFilter true\nEnableCommunityDecorations true\nGameStateUpdateRate 10.0\nGameStateUpdateRateWithConsumers 1.0\nDisableDLCPublishCheck false\nEnableDiveIn true\nEnableHackChecks false\nAllowOnlineCreate true\n" +
$"TelemetryServer {hostname}\n" + $"TelemetryServer {hostname}\n" +
$"CDNHostName {hostname}\n" + $"CDNHostName {hostname}\n" +
$"ShowLevelBoos {ServerConfiguration.Instance.UserGeneratedContentLimits.BooingEnabled.ToString().ToLower()}\n" $"ShowLevelBoos {ServerConfiguration.Instance.UserGeneratedContentLimits.BooingEnabled.ToString().ToLower()}\n"

View file

@ -82,7 +82,7 @@ public class LoginController : ControllerBase
if (ServerConfiguration.Instance.Authentication.UseExternalAuth) if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
{ {
if (this.database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).Select(a => a.IpAddress).Contains(ipAddress)) if (user.ApprovedIPAddress == ipAddress)
{ {
token.Approved = true; token.Approved = true;
} }

View file

@ -4,6 +4,7 @@ using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Serialization;
@ -82,34 +83,67 @@ public class PublishController : ControllerBase
GameToken gameToken = userAndToken.Value.Item2; GameToken gameToken = userAndToken.Value.Item2;
Slot? slot = await this.getSlotFromBody(); Slot? slot = await this.getSlotFromBody();
if (slot == null) return this.BadRequest(); if (slot == null)
{
Logger.Warn("Rejecting level upload, slot is null", LogArea.Publish);
return this.BadRequest();
}
if (slot.Location == null) return this.BadRequest(); if (slot.Location == null)
{
Logger.Warn("Rejecting level upload, slot location is null", LogArea.Publish);
return this.BadRequest();
}
if (slot.Description.Length > 500) return this.BadRequest(); if (slot.Description.Length > 512)
{
Logger.Warn($"Rejecting level upload, description too long ({slot.Description.Length} characters)", LogArea.Publish);
return this.BadRequest();
}
if (slot.Name.Length > 64) return this.BadRequest(); if (slot.Name.Length > 64)
{
Logger.Warn($"Rejecting level upload, title too long ({slot.Name.Length} characters)", LogArea.Publish);
return this.BadRequest();
}
if (slot.Resources.Any(resource => !FileHelper.ResourceExists(resource))) if (slot.Resources.Any(resource => !FileHelper.ResourceExists(resource)))
{ {
Logger.Warn("Rejecting level upload, missing resource(s)", LogArea.Publish);
return this.BadRequest(); return this.BadRequest();
} }
LbpFile? rootLevel = LbpFile.FromHash(slot.RootLevel); LbpFile? rootLevel = LbpFile.FromHash(slot.RootLevel);
if (rootLevel == null) return this.BadRequest(); if (rootLevel == null)
{
Logger.Warn("Rejecting level upload, unable to find rootLevel", LogArea.Publish);
return this.BadRequest();
}
if (rootLevel.FileType != LbpFileType.Level) return this.BadRequest(); if (rootLevel.FileType != LbpFileType.Level)
{
Logger.Warn("Rejecting level upload, rootLevel is not a level", LogArea.Publish);
return this.BadRequest();
}
// Republish logic // Republish logic
if (slot.SlotId != 0) if (slot.SlotId != 0)
{ {
Slot? oldSlot = await this.database.Slots.Include(s => s.Location).FirstOrDefaultAsync(s => s.SlotId == slot.SlotId); Slot? oldSlot = await this.database.Slots.Include(s => s.Location).FirstOrDefaultAsync(s => s.SlotId == slot.SlotId);
if (oldSlot == null) return this.NotFound(); if (oldSlot == null)
{
Logger.Warn("Rejecting level republish, wasn't able to find old slot", LogArea.Publish);
return this.NotFound();
}
if (oldSlot.Location == null) throw new ArgumentNullException(); if (oldSlot.Location == null) throw new ArgumentNullException();
if (oldSlot.CreatorId != user.UserId) return this.BadRequest(); if (oldSlot.CreatorId != user.UserId)
{
Logger.Warn("Rejecting level republish, old level not owned by current user", LogArea.Publish);
return this.BadRequest();
}
oldSlot.Location.X = slot.Location.X; oldSlot.Location.X = slot.Location.X;
oldSlot.Location.Y = slot.Location.Y; oldSlot.Location.Y = slot.Location.Y;
@ -166,7 +200,8 @@ public class PublishController : ControllerBase
if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots) if (user.GetUsedSlotsForGame(gameToken.GameVersion) > ServerConfiguration.Instance.UserGeneratedContentLimits.EntitledSlots)
{ {
return this.StatusCode(403, ""); Logger.Warn("Rejecting level upload, too many published slots", LogArea.Publish);
return this.BadRequest();
} }
//TODO: parse location in body //TODO: parse location in body
@ -197,6 +232,8 @@ public class PublishController : ControllerBase
"New level published!", "New level published!",
$"**{user.Username}** just published a new level: [**{slot.Name}**]({ServerConfiguration.Instance.ExternalUrl}/slot/{slot.SlotId})\n{slot.Description}" $"**{user.Username}** just published a new level: [**{slot.Name}**]({ServerConfiguration.Instance.ExternalUrl}/slot/{slot.SlotId})\n{slot.Description}"
); );
Logger.Success($"Successfully published level {slot.Name} (id: {slot.SlotId}) by {user.Username} (id: {user.UserId})", LogArea.Publish);
return this.Ok(slot.Serialize(gameToken.GameVersion)); return this.Ok(slot.Serialize(gameToken.GameVersion));
} }

View file

@ -99,17 +99,14 @@ public class UserController : ControllerBase
if (update.Biography != null) if (update.Biography != null)
{ {
if (update.Biography.Length > 100) return this.BadRequest(); if (update.Biography.Length > 512) return this.BadRequest();
user.Biography = update.Biography; user.Biography = update.Biography;
} }
foreach (string? resource in new[] foreach (string? resource in new[]{update.IconHash, update.YayHash, update.MehHash, update.BooHash, update.PlanetHash,})
{
update.IconHash, update.YayHash, update.MehHash, update.BooHash, update.PlanetHash,
})
{ {
if (resource != null && !FileHelper.ResourceExists(resource)) return this.BadRequest(); if (resource != null && !resource.StartsWith('g') && !FileHelper.ResourceExists(resource)) return this.BadRequest();
} }
if (update.IconHash != null) user.IconHash = update.IconHash; if (update.IconHash != null) user.IconHash = update.IconHash;

View file

@ -46,4 +46,17 @@ public class RoomVisualizerController : ControllerBase
return this.Redirect("/debug/roomVisualizer"); return this.Redirect("/debug/roomVisualizer");
#endif #endif
} }
[HttpGet("createRoomsWithDuplicatePlayers")]
public async Task<IActionResult> CreateRoomsWithDuplicatePlayers()
{
#if !DEBUG
return this.NotFound();
#else
List<int> users = await this.database.Users.OrderByDescending(_ => EF.Functions.Random()).Take(1).Select(u => u.UserId).ToListAsync();
RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3);
RoomHelper.CreateRoom(users, GameVersion.LittleBigPlanet2, Platform.PS3);
return this.Redirect("/debug/roomVisualizer");
#endif
}
} }

View file

@ -32,15 +32,8 @@ public class AutoApprovalController : ControllerBase
if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login"); if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login");
authAttempt.GameToken.Approved = true; authAttempt.GameToken.Approved = true;
user.ApprovedIPAddress = authAttempt.IPAddress;
UserApprovedIpAddress approvedIpAddress = new()
{
UserId = user.UserId,
User = user,
IpAddress = authAttempt.IPAddress,
};
this.database.UserApprovedIpAddresses.Add(approvedIpAddress);
this.database.AuthenticationAttempts.Remove(authAttempt); this.database.AuthenticationAttempts.Remove(authAttempt);
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
@ -48,20 +41,16 @@ public class AutoApprovalController : ControllerBase
return this.Redirect("/authentication"); return this.Redirect("/authentication");
} }
[HttpGet("revokeAutoApproval/{id:int}")] [HttpGet("revokeAutoApproval")]
public async Task<IActionResult> RevokeAutoApproval([FromRoute] int id) public async Task<IActionResult> RevokeAutoApproval()
{ {
User? user = this.database.UserFromWebRequest(this.Request); User? user = this.database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login"); if (user == null) return this.Redirect("/login");
UserApprovedIpAddress? approvedIpAddress = await this.database.UserApprovedIpAddresses.FirstOrDefaultAsync(a => a.UserApprovedIpAddressId == id); user.ApprovedIPAddress = null;
if (approvedIpAddress == null) return this.BadRequest();
if (approvedIpAddress.UserId != user.UserId) return this.Redirect("/login");
this.database.UserApprovedIpAddresses.Remove(approvedIpAddress);
await this.database.SaveChangesAsync(); await this.database.SaveChangesAsync();
return this.Redirect("/authentication/autoApprovals"); return this.Redirect("/authentication");
} }
} }

View file

@ -1,5 +1,4 @@
using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Files;
using LBPUnion.ProjectLighthouse.Helpers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using IOFile = System.IO.File; using IOFile = System.IO.File;

View file

@ -0,0 +1,56 @@
@page "/admin/keys"
@using LBPUnion.ProjectLighthouse.PlayerData
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminAPIKeyPageModel
@{
Layout = "Layouts/BaseLayout";
Model.Title = "API Keys";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
@{
var token = Antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}
<script>function deleteKey(keyID) {
document.getElementById("trashbutton-".concat(keyID)).classList.add('loading');
fetch("@Url.RouteUrl(ViewContext.RouteData.Values)", {
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
credentials: 'same-origin',
body: 'keyID='.concat(keyID).concat("&__RequestVerificationToken=@token")
})
.then(function (data) {
document.getElementById("keyitem-".concat(keyID)).remove();
window.location.reload(true);
})
.catch(function (error) {
console.log('Request failed', error);
});
}</script>
<p>There are <b>@Model.KeyCount</b> API keys registered.</p>
@if (Model.KeyCount == 0)
{
<p>To create one, you can use the "Create API key" command in the admin panel.</p>
}
<div class="ui four column grid">
@foreach (APIKey key in Model.APIKeys)
{
<div id="keyitem-@key.Id" class="five wide column">
<div class="ui blue segment">
<div class="ui tiny bottom left attached label">
Created at: @key.Created.ToString()
</div>
<button id="trashbutton-@key.Id" class="right floated circular ui icon button" onclick="deleteKey(@key.Id);">
<i class="trash can icon"></i>
</button>
<h2>@key.Description</h2>
</div>
</div>
}
</div>

View file

@ -0,0 +1,43 @@
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin
{
public class AdminAPIKeyPageModel : BaseLayout
{
public List<APIKey> APIKeys = new();
public int KeyCount;
public AdminAPIKeyPageModel(Database database) : base(database)
{ }
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
if (!user.IsAdmin) return this.NotFound();
this.APIKeys = await this.Database.APIKeys.OrderByDescending(k => k.Id).ToListAsync();
this.KeyCount = this.APIKeys.Count;
return this.Page();
}
public async Task<IActionResult> OnPost(string keyID)
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null || !user.IsAdmin) return this.NotFound();
APIKey? apiKey = await this.Database.APIKeys.FirstOrDefaultAsync(k => k.Id == int.Parse(keyID));
if (apiKey == null) return this.NotFound();
this.Database.APIKeys.Remove(apiKey);
await this.Database.SaveChangesAsync();
return this.Page();
}
}
}

View file

@ -15,7 +15,7 @@ public class AdminPanelPage : BaseLayout
{ {
public List<ICommand> Commands = MaintenanceHelper.Commands; public List<ICommand> Commands = MaintenanceHelper.Commands;
public AdminPanelPage(Database database) : base(database) public AdminPanelPage(Database database) : base(database)
{} { }
public List<AdminPanelStatistic> Statistics = new(); public List<AdminPanelStatistic> Statistics = new();
@ -31,6 +31,7 @@ public class AdminPanelPage : BaseLayout
this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount())); this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount()));
this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount())); this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount()));
this.Statistics.Add(new AdminPanelStatistic("Reports", await StatisticsHelper.ReportCount(), "reports/0")); this.Statistics.Add(new AdminPanelStatistic("Reports", await StatisticsHelper.ReportCount(), "reports/0"));
this.Statistics.Add(new AdminPanelStatistic("API Keys", await StatisticsHelper.APIKeyCount(), "keys"));
if (!string.IsNullOrEmpty(command)) if (!string.IsNullOrEmpty(command))
{ {

View file

@ -44,6 +44,10 @@
<div class="ui blue button">Create Fake Room</div> <div class="ui blue button">Create Fake Room</div>
</a> </a>
<a href="/debug/roomVisualizer/createRoomsWithDuplicatePlayers">
<div class="ui blue button">Create Rooms With Duplicate Players</div>
</a>
<a href="/debug/roomVisualizer/deleteRooms"> <a href="/debug/roomVisualizer/deleteRooms">
<div class="ui red button">Nuke all rooms</div> <div class="ui red button">Nuke all rooms</div>
</a> </a>

View file

@ -21,18 +21,24 @@ else
} }
} }
<a href="/authentication/autoApprovals"> @if (Model.User!.ApprovedIPAddress != null)
<button class="ui small blue button"> {
<i class="cog icon"></i> <a href="/authentication/revokeAutoApproval">
<span>Manage automatically approved IP addresses</span> <button class="ui red button">
</button> <i class="trash icon"></i>
</a> <span>Revoke automatically approved IP Address (@Model.User!.ApprovedIPAddress)</span>
<a href="/authentication/denyAll"> </button>
<button class="ui small red button"> </a>
<i class="x icon"></i> }
<span>Deny all</span> @if (Model.AuthenticationAttempts.Count > 1)
</button> {
</a> <a href="/authentication/denyAll">
<button class="ui red button">
<i class="x icon"></i>
<span>Deny all</span>
</button>
</a>
}
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
{ {
@ -41,19 +47,19 @@ else
<p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</b> from the IP address <b>@authAttempt.IPAddress</b>.</p> <p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
<div> <div>
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId"> <a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny green button"> <button class="ui small green button">
<i class="check icon"></i> <i class="check icon"></i>
<span>Automatically approve every time</span> <span>Automatically approve every time</span>
</button> </button>
</a> </a>
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId"> <a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny yellow button"> <button class="ui small yellow button">
<i class="check icon"></i> <i class="check icon"></i>
<span>Approve this time</span> <span>Approve this time</span>
</button> </button>
</a> </a>
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId"> <a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
<button class="ui tiny red button"> <button class="ui small red button">
<i class="x icon"></i> <i class="x icon"></i>
<span>Deny</span> <span>Deny</span>
</button> </button>

View file

@ -1,23 +0,0 @@
@page "/authentication/autoApprovals"
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth.ManageUserApprovedIpAddressesPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Automatically approved IP addresses";
}
@foreach (UserApprovedIpAddress approvedIpAddress in Model.ApprovedIpAddresses)
{
<div class="ui blue segment">
<p>@approvedIpAddress.IpAddress</p>
<a href="/authentication/revokeAutoApproval/@approvedIpAddress.UserApprovedIpAddressId">
<button class="ui red button">
<i class="trash icon"></i>
<span>Revoke</span>
</button>
</a>
</div>
}

View file

@ -1,26 +0,0 @@
#nullable enable
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth;
public class ManageUserApprovedIpAddressesPage : BaseLayout
{
public List<UserApprovedIpAddress> ApprovedIpAddresses = new();
public ManageUserApprovedIpAddressesPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login");
this.ApprovedIpAddresses = await this.Database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).ToListAsync();
return this.Page();
}
}

View file

@ -45,7 +45,7 @@ else
} }
} }
<br> <br><br>
<div class="@(isMobile ? "" : "ui center aligned grid")"> <div class="@(isMobile ? "" : "ui center aligned grid")">
<div class="eight wide column"> <div class="eight wide column">

View file

@ -26,20 +26,23 @@
} }
Model.IsMobile = Model.Request.IsMobile(); Model.IsMobile = Model.Request.IsMobile();
string title;
if (Model.Title == string.Empty)
{
title = ServerConfiguration.Instance.Customization.ServerName;
}
else
{
title = $"{Model.Title} - {ServerConfiguration.Instance.Customization.ServerName}";
}
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@if (Model.Title == string.Empty) <title>@title</title>
{
<title>@ServerConfiguration.Instance.Customization.ServerName</title>
}
else
{
<title>@ServerConfiguration.Instance.Customization.ServerName - @Model.Title</title>
}
<link rel="stylesheet" type="text/css" href="~/css/styles.css"> <link rel="stylesheet" type="text/css" href="~/css/styles.css">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css"> <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css">
@ -53,7 +56,7 @@
@* Embed Stuff *@ @* Embed Stuff *@
<meta name="theme-color" data-react-helmet="true" content="#008cff"> <meta name="theme-color" data-react-helmet="true" content="#008cff">
<meta content="@ServerConfiguration.Instance.Customization.ServerName - @Model.Title" property="og:title"> <meta content="@title" property="og:title">
@if (!string.IsNullOrEmpty(Model.Description)) @if (!string.IsNullOrEmpty(Model.Description))
{ {
<meta content="@Model.Description" property="og:description"> <meta content="@Model.Description" property="og:description">

View file

@ -11,11 +11,16 @@
<script> <script>
function onSubmit(form) { function onSubmit(form) {
if (document.referrer !== null && document.referrer !== "") {
const url = new URL(document.referrer);
if (url.pathname !== "/logout" && url.pathname !== "/login")
document.getElementById("redirect").value = url.pathname;
}
const passwordInput = document.getElementById("password"); const passwordInput = document.getElementById("password");
const passwordSubmit = document.getElementById("password-submit"); const passwordSubmit = document.getElementById("password-submit");
passwordSubmit.value = sha256(passwordInput.value); passwordSubmit.value = sha256(passwordInput.value);
return true; return true;
} }
</script> </script>
@ -32,6 +37,7 @@
<form class="ui form" onsubmit="return onSubmit(this)" method="post"> <form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<input type="hidden" id="redirect" name="redirect">
<div class="field"> <div class="field">
<label>Username</label> <label>Username</label>
@ -54,15 +60,23 @@
{ {
@await Html.PartialAsync("Partials/CaptchaPartial") @await Html.PartialAsync("Partials/CaptchaPartial")
} }
<input type="submit" value="Log in" id="submit" class="ui blue button"> <div class="row">
@if (ServerConfiguration.Instance.Authentication.RegistrationEnabled) <input type="submit" value="Log in" id="submit" class="ui blue button">
{ @if (ServerConfiguration.Instance.Authentication.RegistrationEnabled)
<a href="/register"> {
<a href="/register">
<div class="ui button">
<i class="user alternate add icon"></i>
Register
</div>
</a>
}
</div>
<br/>
<a href="/passwordResetRequest">
<div class="ui button"> <div class="ui button">
<i class="user alternate add icon"></i> Forgot Password?
Register
</div> </div>
</a> </a>
}
</form> </form>

View file

@ -22,7 +22,7 @@ public class LoginForm : BaseLayout
public string? Error { get; private set; } public string? Error { get; private set; }
[UsedImplicitly] [UsedImplicitly]
public async Task<IActionResult> OnPost(string username, string password) public async Task<IActionResult> OnPost(string username, string password, string redirect)
{ {
if (string.IsNullOrWhiteSpace(username)) if (string.IsNullOrWhiteSpace(username))
{ {
@ -105,9 +105,19 @@ public class LoginForm : BaseLayout
if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired"); if (user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
if (ServerConfiguration.Instance.Mail.MailEnabled && !user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail"); if (ServerConfiguration.Instance.Mail.MailEnabled && !user.EmailAddressVerified) return this.Redirect("~/login/sendVerificationEmail");
return this.RedirectToPage(nameof(LandingPage)); if (string.IsNullOrWhiteSpace(redirect))
{
return this.RedirectToPage(nameof(LandingPage));
}
return this.Redirect(redirect);
} }
[UsedImplicitly] [UsedImplicitly]
public IActionResult OnGet() => this.Page(); public IActionResult OnGet()
{
if (this.Database.UserFromWebRequest(this.Request) != null)
return this.RedirectToPage(nameof(LandingPage));
return this.Page();
}
} }

View file

@ -1,6 +1,17 @@
@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Configuration
@if (ServerConfiguration.Instance.Captcha.CaptchaEnabled) @if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{ {
<div class="h-captcha" data-sitekey="@ServerConfiguration.Instance.Captcha.SiteKey"></div> @switch (ServerConfiguration.Instance.Captcha.Type)
<script src="https://js.hcaptcha.com/1/api.js" async defer></script> {
case CaptchaType.HCaptcha:
<div class="h-captcha" data-sitekey="@ServerConfiguration.Instance.Captcha.SiteKey"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
break;
case CaptchaType.ReCaptcha:
<div class="g-recaptcha" data-sitekey="@ServerConfiguration.Instance.Captcha.SiteKey"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
break;
default:
throw new ArgumentOutOfRangeException();
}
} }

View file

@ -1,4 +1,5 @@
@using System.Web @using System.Web
@using System.IO
@using LBPUnion.ProjectLighthouse.PlayerData.Profiles @using LBPUnion.ProjectLighthouse.PlayerData.Profiles
<div class="ui yellow segment" id="comments"> <div class="ui yellow segment" id="comments">
<h2>Comments</h2> <h2>Comments</h2>
@ -47,7 +48,14 @@
int rating = comment.ThumbsUp - comment.ThumbsDown; int rating = comment.ThumbsUp - comment.ThumbsDown;
<div style="display: flex" id="@comment.CommentId"> <div style="display: flex" id="@comment.CommentId">
<div class="voting"> @{
string style = "";
if (Model.User?.UserId == comment.PosterUserId)
{
style = "visibility: hidden";
}
}
<div class="voting" style="@(style)">
<a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == 1 ? 0 : 1)"> <a href="@url/rateComment?commentId=@(comment.CommentId)&rating=@(comment.YourThumb == 1 ? 0 : 1)">
<i class="fitted @(comment.YourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i> <i class="fitted @(comment.YourThumb == 1 ? "green" : "grey") arrow up link icon" style="display: block"></i>
</a> </a>

View file

@ -1,3 +1,4 @@
@using System.Web
@using LBPUnion.ProjectLighthouse @using LBPUnion.ProjectLighthouse
@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.PlayerData
@ -10,8 +11,8 @@
await using Database database = new(); await using Database database = new();
string slotName = string.IsNullOrEmpty(Model.Name) ? "Unnamed Level" : Model.Name; string slotName = HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model!.Name) ? "Unnamed Level" : Model.Name);
bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool isMobile = (bool?)ViewData["IsMobile"] ?? false;
bool mini = (bool?)ViewData["IsMini"] ?? false; bool mini = (bool?)ViewData["IsMini"] ?? false;
@ -36,7 +37,9 @@
} }
<div> <div>
<img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute"> <img src="~/assets/slotCardOverlay.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; pointer-events: none; position: absolute">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px"> <img src="~/assets/slotCardBackground.png" style="min-width: @(size)px; width: @(size)px; height: @(size)px; position: absolute; z-index: -1;">
<img class="cardIcon slotCardIcon" src="/gameAssets/@iconHash" style="min-width: @(size)px; width: @(size)px; height: @(size)px;"
onerror="this.onerror='';this.src='/gameAssets/@ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash'">
</div> </div>
<div class="cardStats"> <div class="cardStats">
@if (!mini) @if (!mini)
@ -79,7 +82,7 @@
@if (Model.GameVersion == GameVersion.LittleBigPlanet1) @if (Model.GameVersion == GameVersion.LittleBigPlanet1)
{ {
<i class="yellow star icon" title="LBP1 Stars"></i> <i class="yellow star icon" title="LBP1 Stars"></i>
<span>@Model.RatingLBP1</span> <span>@(Math.Round(Model.RatingLBP1 * 10) / 10)</span>
} }
</div> </div>
<p> <p>

View file

@ -4,7 +4,6 @@ using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
@ -19,8 +18,21 @@ public class PasswordResetPage : BaseLayout
[UsedImplicitly] [UsedImplicitly]
public async Task<IActionResult> OnPost(string password, string confirmPassword) public async Task<IActionResult> OnPost(string password, string confirmPassword)
{ {
User? user = this.Database.UserFromWebRequest(this.Request); User? user;
if (user == null) return this.Redirect("~/login"); if (Request.Query.ContainsKey("token"))
{
user = await this.Database.UserFromPasswordResetToken(Request.Query["token"][0]);
if (user == null)
{
this.Error = "This password reset link either is invalid or has expired. Please try again.";
return this.Page();
}
}
else
{
user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login");
}
if (string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(password))
{ {
@ -48,6 +60,8 @@ public class PasswordResetPage : BaseLayout
[UsedImplicitly] [UsedImplicitly]
public IActionResult OnGet() public IActionResult OnGet()
{ {
if (this.Request.Query.ContainsKey("token")) return this.Page();
User? user = this.Database.UserFromWebRequest(this.Request); User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("~/login"); if (user == null) return this.Redirect("~/login");

View file

@ -0,0 +1,34 @@
@page "/passwordResetRequest"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequestForm
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Password Reset";
}
@if (!string.IsNullOrWhiteSpace(Model.Error))
{
<div class="ui negative message">
<div class="header">
Uh oh!
</div>
<p style="white-space: pre-line">@Model.Error</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.Status))
{
<div class="ui positive message">
<div class="header">
Success!
</div>
<p style="white-space: pre-line">@Model.Status</p>
</div>
}
<form class="ui form" method="post">
@Html.AntiForgeryToken()
<input type="text" autocomplete="no" id="username" placeholder="Username" name="username"/><br/><br/>
<input type="submit" value="Request Password Reset" class="ui blue button"/>
</form>

View file

@ -0,0 +1,67 @@
using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class PasswordResetRequestForm : BaseLayout
{
public string? Error { get; private set; }
public string? Status { get; private set; }
public PasswordResetRequestForm(Database database) : base(database)
{ }
[UsedImplicitly]
public async Task<IActionResult> OnPost(string username)
{
if (!ServerConfiguration.Instance.Mail.MailEnabled)
{
this.Error = "Email is not configured on this server, so password resets cannot be issued. Please contact your instance administrator for more details.";
return this.Page();
}
if (string.IsNullOrWhiteSpace(username))
{
this.Error = "The username field is required.";
return this.Page();
}
User? user = await this.Database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null)
{
this.Error = "User does not exist.";
return this.Page();
}
PasswordResetToken token = new()
{
Created = DateTime.Now,
UserId = user.UserId,
ResetToken = CryptoHelper.GenerateAuthToken(),
};
string messageBody = $"Hello, {user.Username}.\n\n" +
"A request to reset your account's password was issued. If this wasn't you, this can probably be ignored.\n\n" +
$"If this was you, your {ServerConfiguration.Instance.Customization.ServerName} password can be reset at the following link:\n" +
$"{ServerConfiguration.Instance.ExternalUrl}/passwordReset?token={token.ResetToken}";
SMTPHelper.SendEmail(user.EmailAddress, $"Project Lighthouse Password Reset Request for {user.Username}", messageBody);
this.Database.PasswordResetTokens.Add(token);
await this.Database.SaveChangesAsync();
this.Status = $"Password reset email sent to {CensorHelper.MaskEmail(user.EmailAddress)}.";
return this.Page();
}
public void OnGet() => this.Page();
}

View file

@ -12,7 +12,7 @@
<form action="/photos/0"> <form action="/photos/0">
<div class="ui icon input"> <div class="ui icon input">
<input type="text" name="name" placeholder="Search photos..." value="@Model.SearchValue"> <input type="text" autocomplete="off" name="name" placeholder="Search photos..." value="@Model.SearchValue">
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
</form> </form>

View file

@ -15,7 +15,7 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class RegisterForm : BaseLayout public class RegisterForm : BaseLayout
{ {
public RegisterForm(Database database) : base(database) public RegisterForm(Database database) : base(database)
{} { }
public string? Error { get; private set; } public string? Error { get; private set; }
@ -23,7 +23,22 @@ public class RegisterForm : BaseLayout
[SuppressMessage("ReSharper", "SpecifyStringComparison")] [SuppressMessage("ReSharper", "SpecifyStringComparison")]
public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress) public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress)
{ {
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound(); if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
{
if (this.Request.Query.ContainsKey("token"))
{
if (!this.Database.IsRegistrationTokenValid(this.Request.Query["token"]))
return this.StatusCode(403, "Invalid Token");
}
else
{
return this.NotFound();
}
}
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
{
return this.NotFound();
}
if (string.IsNullOrWhiteSpace(username)) if (string.IsNullOrWhiteSpace(username))
{ {
@ -68,6 +83,11 @@ public class RegisterForm : BaseLayout
return this.Page(); return this.Page();
} }
if (this.Request.Query.ContainsKey("token"))
{
await Database.RemoveRegistrationToken(this.Request.Query["token"]);
}
User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress); User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);
WebToken webToken = new() WebToken webToken = new()
@ -91,7 +111,22 @@ public class RegisterForm : BaseLayout
public IActionResult OnGet() public IActionResult OnGet()
{ {
this.Error = string.Empty; this.Error = string.Empty;
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound(); if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
{
if (this.Request.Query.ContainsKey("token"))
{
if (!this.Database.IsRegistrationTokenValid(this.Request.Query["token"]))
return this.StatusCode(403, "Invalid Token");
}
else
{
return this.NotFound();
}
}
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
{
return this.NotFound();
}
return this.Page(); return this.Page();
} }

View file

@ -11,7 +11,7 @@
<form action="/admin/reports/0"> <form action="/admin/reports/0">
<div class="ui icon input"> <div class="ui icon input">
<input type="text" name="name" placeholder="Search reports..." value="@Model.SearchValue"> <input type="text" autocomplete="off" name="name" placeholder="Search reports..." value="@Model.SearchValue">
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
</form> </form>

View file

@ -4,15 +4,14 @@
@using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData.Reviews @using LBPUnion.ProjectLighthouse.PlayerData.Reviews
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage
@{ @{
Layout = "Layouts/BaseLayout"; Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false; Model.ShowTitleInPage = false;
Model.Title = Model.Slot?.Name ?? ""; Model.Title = HttpUtility.HtmlDecode(Model.Slot?.Name ?? "");
Model.Description = Model.Slot?.Description ?? ""; Model.Description = HttpUtility.HtmlDecode(Model.Slot?.Description ?? "");
bool isMobile = this.Request.IsMobile(); bool isMobile = this.Request.IsMobile();
} }
@ -38,15 +37,14 @@
<div class="eight wide column"> <div class="eight wide column">
<div class="ui blue segment"> <div class="ui blue segment">
<h2>Description</h2> <h2>Description</h2>
<p>@HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model.Slot?.Description) ? "This level has no description." : Model.Slot.Description)</p> <p style="overflow-wrap: anywhere">@HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model.Slot?.Description) ? "This level has no description." : Model.Slot.Description)</p>
</div> </div>
</div> </div>
<div class="eight wide column"> <div class="eight wide column">
<div class="ui red segment"> <div class="ui red segment">
<h2>Tags</h2> <h2>Tags</h2>
@{ @{
string[] authorLabels = Model.Slot?.AuthorLabels.Split(",") ?? new string[] string[] authorLabels = Model.Slot?.AuthorLabels.Split(",") ?? new string[]{};
{};
if (authorLabels.Length == 1) // ..?? ok c# if (authorLabels.Length == 1) // ..?? ok c#
{ {
<p>This level has no tags.</p> <p>This level has no tags.</p>

View file

@ -12,7 +12,7 @@
<form action="/slots/0"> <form action="/slots/0">
<div class="ui icon input"> <div class="ui icon input">
<input type="text" name="name" placeholder="Search levels..." value="@Model.SearchValue"> <input type="text" autocomplete="off" name="name" placeholder="Search levels..." value="@Model.SearchValue">
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
</form> </form>

View file

@ -13,7 +13,7 @@
<form action="/users/0"> <form action="/users/0">
<div class="ui icon input"> <div class="ui icon input">
<input type="text" name="name" placeholder="Search users..." value="@Model.SearchValue"> <input type="text" autocomplete="off" name="name" placeholder="Search users..." value="@Model.SearchValue">
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
</form> </form>

View file

@ -34,7 +34,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.7" />
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View file

@ -58,7 +58,10 @@ public class WebsiteStartup
app.UseRouting(); app.UseRouting();
app.UseStaticFiles(); app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true,
});
app.UseRequestLocalization(); app.UseRequestLocalization();

View file

@ -9,8 +9,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -9,14 +9,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.2.0" /> <PackageReference Include="Selenium.WebDriver" Version="4.3.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="103.0.5060.13400" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -14,8 +14,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.Helpers;
namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands
{
public class CreateAPIKeyCommand : ICommand
{
public string Name() => "Create API Key";
public string[] Aliases() => new[] { "createAPIKey", };
public string Arguments() => "<description>";
public int RequiredArgs() => 1;
public async Task Run(string[] args, Logger logger)
{
APIKey key = new();
key.Description = args[0];
if (string.IsNullOrWhiteSpace(key.Description))
{
key.Description = "<no description specified>";
}
key.Key = CryptoHelper.GenerateAuthToken();
key.Created = DateTime.Now;
Database database = new();
await database.APIKeys.AddAsync(key);
await database.SaveChangesAsync();
logger.LogSuccess($"The API key has been created (id: {key.Id}), however for security the token will only be shown once.", LogArea.Command);
logger.LogInfo($"Key: {key.Key}", LogArea.Command);
}
}
}

View file

@ -0,0 +1,16 @@
namespace LBPUnion.ProjectLighthouse.Configuration;
/// <summary>
/// The service to be used for presenting captchas to the user.
/// </summary>
public enum CaptchaType
{
/// <summary>
/// A privacy-first captcha. https://www.hcaptcha.com/
/// </summary>
HCaptcha,
/// <summary>
/// A captcha service by Google. https://developers.google.com/recaptcha/
/// </summary>
ReCaptcha,
}

View file

@ -8,5 +8,6 @@ public class AuthenticationConfiguration
public bool BlockDeniedUsers { get; set; } public bool BlockDeniedUsers { get; set; }
public bool RegistrationEnabled { get; set; } = true; public bool RegistrationEnabled { get; set; } = true;
public bool PrivateRegistration { get; set; } = false;
public bool UseExternalAuth { get; set; } public bool UseExternalAuth { get; set; }
} }

View file

@ -2,11 +2,10 @@ namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
public class CaptchaConfiguration public class CaptchaConfiguration
{ {
// TODO: support recaptcha, not just hcaptcha
// use an enum to define which captcha services can be used?
// LBPUnion.ProjectLighthouse.Types.Settings.CaptchaService
public bool CaptchaEnabled { get; set; } public bool CaptchaEnabled { get; set; }
public CaptchaType Type { get; set; } = CaptchaType.HCaptcha;
public string SiteKey { get; set; } = ""; public string SiteKey { get; set; } = "";
public string Secret { get; set; } = ""; public string Secret { get; set; } = "";

View file

@ -23,7 +23,7 @@ public class ServerConfiguration
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though. // You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
// //
// Thanks for listening~ // Thanks for listening~
public const int CurrentConfigVersion = 5; public const int CurrentConfigVersion = 7;
#region Meta #region Meta
@ -42,7 +42,7 @@ public class ServerConfiguration
private static FileSystemWatcher fileWatcher; private static FileSystemWatcher fileWatcher;
// ReSharper disable once NotNullMemberIsNotInitialized // ReSharper disable once NotNullMemberIsNotInitialized
#pragma warning disable CS8618 #pragma warning disable CS8618
static ServerConfiguration() static ServerConfiguration()
{ {
if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own
@ -54,7 +54,7 @@ public class ServerConfiguration
// If a valid YML configuration is available! // If a valid YML configuration is available!
if (File.Exists(ConfigFileName) && (tempConfig = fromFile(ConfigFileName)) != null) if (File.Exists(ConfigFileName) && (tempConfig = fromFile(ConfigFileName)) != null)
{ {
// Instance = JsonSerializer.Deserialize<ServerConfiguration>(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName)); // Instance = JsonSerializer.Deserialize<ServerConfiguration>(configFile) ?? throw new ArgumentNullException(nameof(ConfigFileName));
Instance = tempConfig; Instance = tempConfig;
if (Instance.ConfigVersion < CurrentConfigVersion) if (Instance.ConfigVersion < CurrentConfigVersion)
@ -114,7 +114,7 @@ public class ServerConfiguration
fileWatcher.EnableRaisingEvents = true; // begin watching fileWatcher.EnableRaisingEvents = true; // begin watching
} }
} }
#pragma warning restore CS8618 #pragma warning restore CS8618
private static void onConfigChanged(object sender, FileSystemEventArgs e) private static void onConfigChanged(object sender, FileSystemEventArgs e)
{ {
@ -178,11 +178,11 @@ public class ServerConfiguration
public string ExternalUrl { get; set; } = "http://localhost:10060"; public string ExternalUrl { get; set; } = "http://localhost:10060";
public bool ConfigReloading { get; set; } public bool ConfigReloading { get; set; }
public string EulaText { get; set; } = ""; public string EulaText { get; set; } = "";
#if !DEBUG #if !DEBUG
public string AnnounceText { get; set; } = "You are now logged in as %user."; public string AnnounceText { get; set; } = "You are now logged in as %user.";
#else #else
public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id)."; public string AnnounceText { get; set; } = "You are now logged in as %user (id: %id).";
#endif #endif
public bool CheckForUnsafeFiles { get; set; } = true; public bool CheckForUnsafeFiles { get; set; } = true;
public FilterMode UserInputFilterMode { get; set; } = FilterMode.None; public FilterMode UserInputFilterMode { get; set; } = FilterMode.None;

View file

@ -41,17 +41,19 @@ public class Database : DbContext
public DbSet<AuthenticationAttempt> AuthenticationAttempts { get; set; } public DbSet<AuthenticationAttempt> AuthenticationAttempts { get; set; }
public DbSet<Review> Reviews { get; set; } public DbSet<Review> Reviews { get; set; }
public DbSet<RatedReview> RatedReviews { get; set; } public DbSet<RatedReview> RatedReviews { get; set; }
public DbSet<UserApprovedIpAddress> UserApprovedIpAddresses { get; set; }
public DbSet<DatabaseCategory> CustomCategories { get; set; } public DbSet<DatabaseCategory> CustomCategories { get; set; }
public DbSet<Reaction> Reactions { get; set; } public DbSet<Reaction> Reactions { get; set; }
public DbSet<GriefReport> Reports { get; set; } public DbSet<GriefReport> Reports { get; set; }
public DbSet<EmailVerificationToken> EmailVerificationTokens { get; set; } public DbSet<EmailVerificationToken> EmailVerificationTokens { get; set; }
public DbSet<EmailSetToken> EmailSetTokens { get; set; } public DbSet<EmailSetToken> EmailSetTokens { get; set; }
public DbSet<PasswordResetToken> PasswordResetTokens { get; set; }
public DbSet<RegistrationToken> RegistrationTokens { get; set; }
public DbSet<APIKey> APIKeys { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); => options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
#nullable enable #nullable enable
public async Task<User> CreateUser(string username, string password, string? emailAddress = null) public async Task<User> CreateUser(string username, string password, string? emailAddress = null)
{ {
if (!password.StartsWith('$')) throw new ArgumentException(nameof(password) + " is not a BCrypt hash"); if (!password.StartsWith('$')) throw new ArgumentException(nameof(password) + " is not a BCrypt hash");
@ -357,6 +359,49 @@ public class Database : DbContext
return this.WebTokens.FirstOrDefault(t => t.UserToken == lighthouseToken); return this.WebTokens.FirstOrDefault(t => t.UserToken == lighthouseToken);
} }
public async Task<User?> UserFromPasswordResetToken(string resetToken)
{
PasswordResetToken? token = await this.PasswordResetTokens.FirstOrDefaultAsync(token => token.ResetToken == resetToken);
if (token == null)
{
return null;
}
if (token.Created < DateTime.Now.AddHours(-1)) // if token is expired
{
this.PasswordResetTokens.Remove(token);
return null;
}
return await this.Users.FirstOrDefaultAsync(user => user.UserId == token.UserId);
}
public bool IsRegistrationTokenValid(string tokenString)
{
RegistrationToken? token = this.RegistrationTokens.FirstOrDefault(t => t.Token == tokenString);
if (token == null) return false;
if (token.Created < DateTime.Now.AddDays(-7)) // if token is expired
{
this.RegistrationTokens.Remove(token);
return false;
}
return true;
}
public async Task RemoveRegistrationToken(string tokenString)
{
RegistrationToken? token = await this.RegistrationTokens.FirstOrDefaultAsync(t => t.Token == tokenString);
if (token == null) return;
this.RegistrationTokens.Remove(token);
await this.SaveChangesAsync();
}
#endregion #endregion
public async Task<Photo?> PhotoFromSubject(PhotoSubject subject) public async Task<Photo?> PhotoFromSubject(PhotoSubject subject)
@ -398,5 +443,5 @@ public class Database : DbContext
if (saveChanges) await this.SaveChangesAsync(); if (saveChanges) await this.SaveChangesAsync();
} }
#nullable disable #nullable disable
} }

View file

@ -15,7 +15,21 @@ namespace LBPUnion.ProjectLighthouse.Extensions;
public static class RequestExtensions public static class RequestExtensions
{ {
static RequestExtensions()
{
Uri captchaUri = ServerConfiguration.Instance.Captcha.Type switch
{
CaptchaType.HCaptcha => new Uri("https://hcaptcha.com"),
CaptchaType.ReCaptcha => new Uri("https://www.google.com/recaptcha/api/"),
_ => throw new ArgumentOutOfRangeException(),
};
client = new HttpClient
{
BaseAddress = captchaUri,
};
}
#region Mobile Checking #region Mobile Checking
// yoinked and adapted from https://stackoverflow.com/a/68641796 // yoinked and adapted from https://stackoverflow.com/a/68641796
@ -32,10 +46,7 @@ public static class RequestExtensions
#region Captcha #region Captcha
private static readonly HttpClient client = new() private static readonly HttpClient client;
{
BaseAddress = new Uri("https://hcaptcha.com"),
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
private static async Task<bool> verifyCaptcha(string token) private static async Task<bool> verifyCaptcha(string token)
@ -48,7 +59,7 @@ public static class RequestExtensions
new("response", token), new("response", token),
}; };
HttpResponseMessage response = await client.PostAsync("/siteverify", new FormUrlEncodedContent(payload)); HttpResponseMessage response = await client.PostAsync("siteverify", new FormUrlEncodedContent(payload));
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -63,7 +74,14 @@ public static class RequestExtensions
{ {
if (ServerConfiguration.Instance.Captcha.CaptchaEnabled) if (ServerConfiguration.Instance.Captcha.CaptchaEnabled)
{ {
bool gotCaptcha = request.Form.TryGetValue("h-captcha-response", out StringValues values); string keyName = ServerConfiguration.Instance.Captcha.Type switch
{
CaptchaType.HCaptcha => "h-captcha-response",
CaptchaType.ReCaptcha => "g-recaptcha-response",
_ => throw new ArgumentOutOfRangeException(),
};
bool gotCaptcha = request.Form.TryGetValue(keyName, out StringValues values);
if (!gotCaptcha) return false; if (!gotCaptcha) return false;
if (!await verifyCaptcha(values[0])) return false; if (!await verifyCaptcha(values[0])) return false;

View file

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using System.Text; using System.Text;
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Types; using LBPUnion.ProjectLighthouse.Types;
@ -94,4 +95,23 @@ public static class CensorHelper
return sb.ToString(); return sb.ToString();
} }
public static string MaskEmail(string email)
{
if (string.IsNullOrEmpty(email) || !email.Contains('@')) return email;
string[] emailArr = email.Split('@');
string domainExt = Path.GetExtension(email);
string maskedEmail = string.Format("{0}****{1}@{2}****{3}{4}",
emailArr[0][0],
emailArr[0].Substring(emailArr[0].Length - 1),
emailArr[1][0],
emailArr[1]
.Substring(emailArr[1].Length - domainExt.Length - 1,
1),
domainExt);
return maskedEmail;
}
} }

View file

@ -36,6 +36,8 @@ public static class MatchHelper
return recentlyDivedIn.Contains(otherUserId); return recentlyDivedIn.Contains(otherUserId);
} }
public static bool ClearUserRecentDiveIns(int userId) => UserRecentlyDivedIn.Remove(userId);
// This is the function used to show people how laughably awful LBP's protocol is. Beware. // This is the function used to show people how laughably awful LBP's protocol is. Beware.
public static IMatchCommand? Deserialize(string data) public static IMatchCommand? Deserialize(string data)
{ {

View file

@ -25,4 +25,6 @@ public static class StatisticsHelper
public static async Task<int> PhotoCount() => await database.Photos.CountAsync(); public static async Task<int> PhotoCount() => await database.Photos.CountAsync();
public static async Task<int> ReportCount() => await database.Reports.CountAsync(); public static async Task<int> ReportCount() => await database.Reports.CountAsync();
public static async Task<int> APIKeyCount() => await database.APIKeys.CountAsync();
} }

View file

@ -21,4 +21,5 @@ public enum LogArea
Redis, Redis,
Command, Command,
Admin, Admin,
Publish,
} }

View file

@ -43,7 +43,18 @@ public class Room
public bool IsLookingForPlayers => this.State == RoomState.PlayingLevel || this.State == RoomState.DivingInWaiting; public bool IsLookingForPlayers => this.State == RoomState.PlayingLevel || this.State == RoomState.DivingInWaiting;
[JsonIgnore] [JsonIgnore]
public int HostId => this.PlayerIds[0]; public int HostId {
get {
try
{
return this.PlayerIds[0];
}
catch
{
return -1;
}
}
}
#nullable enable #nullable enable
public override bool Equals(object? obj) public override bool Equals(object? obj)

View file

@ -1,5 +1,7 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -46,10 +48,10 @@ public class RoomHelper
return null; return null;
} }
IEnumerable<Room> rooms = Rooms; Random random = new();
IEnumerable<Room> rooms = Rooms.OrderBy(_ => random.Next());
rooms = rooms.OrderBy(r => r.IsLookingForPlayers); rooms = rooms.OrderBy(r => r.IsLookingForPlayers);
rooms = rooms.Where(r => r.RoomVersion == roomVersion).ToList(); rooms = rooms.Where(r => r.RoomVersion == roomVersion).ToList();
if (platform != null) rooms = rooms.Where(r => r.RoomPlatform == platform).ToList(); if (platform != null) rooms = rooms.Where(r => r.RoomPlatform == platform).ToList();
@ -136,6 +138,12 @@ public class RoomHelper
return response; return response;
} }
if (user != null)
{
MatchHelper.ClearUserRecentDiveIns(user.UserId);
Logger.Info($"Cleared {user.Username} (id: {user.UserId})'s recent dive-ins", LogArea.Match);
}
return null; return null;
} }
@ -194,49 +202,94 @@ public class RoomHelper
[SuppressMessage("ReSharper", "InvertIf")] [SuppressMessage("ReSharper", "InvertIf")]
public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null) public static void CleanupRooms(int? hostId = null, Room? newRoom = null, Database? database = null)
{ {
#if DEBUG
Stopwatch stopwatch = new();
stopwatch.Start();
#endif
lock(RoomLock) lock(RoomLock)
{ {
int roomCountBeforeCleanup = Rooms.Count(); 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
int roomCountBeforeCleanup = rooms.Count();
// Remove offline players from rooms // Remove offline players from rooms
foreach (Room room in Rooms) foreach (Room room in rooms)
{ {
List<User> players = room.GetPlayers(database ?? new Database()); List<User> players = room.GetPlayers(database ?? new Database());
List<int> playersToRemove = players.Where(player => player.Status.StatusType == StatusType.Offline).Select(player => player.UserId).ToList(); List<int> playersToRemove = players.Where(player => player.Status.StatusType == StatusType.Offline).Select(player => player.UserId).ToList();
foreach (int player in playersToRemove) room.PlayerIds.Remove(player); foreach (int player in playersToRemove) room.PlayerIds.Remove(player);
roomsToUpdate.Add(room);
}
// DO NOT REMOVE ROOMS BEFORE THIS POINT!
// this will cause the room to be added back to the database
foreach (Room room in roomsToUpdate)
{
rooms.Update(room);
} }
// Delete old rooms based on host // Delete old rooms based on host
if (hostId != null) if (hostId != null)
{
try try
{ {
Rooms.RemoveAll(r => r.HostId == hostId); rooms.RemoveAll(r => r.PlayerIds.Contains((int)hostId));
} }
catch catch
{ {
// TODO: detect the room that failed and remove it // TODO: detect the room that failed and remove it
} }
}
// Remove players in this new room from other rooms // Remove rooms containing players in this new room
if (newRoom != null) if (newRoom != null)
foreach (Room room in Rooms)
{
if (room == newRoom) continue;
foreach (int newRoomPlayer in newRoom.PlayerIds) room.PlayerIds.RemoveAll(p => p == newRoomPlayer);
}
Rooms.RemoveAll(r => r.PlayerIds.Count == 0); // Remove empty rooms
Rooms.RemoveAll(r => r.PlayerIds.Count > 4); // Remove obviously bogus rooms
int roomCountAfterCleanup = Rooms.Count();
if (roomCountBeforeCleanup != roomCountAfterCleanup)
{ {
Logger.Debug($"Cleaned up {roomCountBeforeCleanup - roomCountAfterCleanup} rooms.", foreach (Room room in rooms.Where(room => room != newRoom))
LogArea.Match); {
foreach (int newRoomPlayer in newRoom.PlayerIds)
{
if (room.PlayerIds.Contains(newRoomPlayer)) rooms.Remove(room);
}
}
}
rooms.RemoveAll(r => r.PlayerIds.Count == 0); // Remove empty rooms
rooms.RemoveAll(r => r.HostId == -1); // Remove rooms with broken hosts
rooms.RemoveAll(r => r.PlayerIds.Count > 4); // Remove obviously bogus rooms
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
// production environment but it's still quite useful for debugging.
//
// So, we handle that case here:
int roomsCleanedUp = roomCountBeforeCleanup - roomCountAfterCleanup;
string logText = $"Cleaned up {roomsCleanedUp} rooms.";
if (roomsCleanedUp == 0)
{
Logger.Debug(logText, LogArea.Match);
}
else
{
Logger.Info(logText, LogArea.Match);
}
logText = $"Updated {roomsToUpdate.Count} rooms.";
if (roomsToUpdate.Count == 0)
{
Logger.Debug(logText, LogArea.Match);
}
else
{
Logger.Info(logText, LogArea.Match);
} }
} }
} }

View file

@ -0,0 +1,61 @@
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220611221819_OnlyAllowSingleApprovedIP")]
public class OnlyAllowSingleApprovedIP : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserApprovedIpAddresses");
migrationBuilder.AddColumn<string>(
name: "ApprovedIPAddress",
table: "Users",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ApprovedIPAddress",
table: "Users");
migrationBuilder.CreateTable(
name: "UserApprovedIpAddresses",
columns: table => new
{
UserApprovedIpAddressId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
IpAddress = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_UserApprovedIpAddresses", x => x.UserApprovedIpAddressId);
table.ForeignKey(
name: "FK_UserApprovedIpAddresses_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_UserApprovedIpAddresses_UserId",
table: "UserApprovedIpAddresses",
column: "UserId");
}
}
}

View file

@ -0,0 +1,41 @@
using System;
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220624210701_AddedPasswordResetTokens")]
public partial class AddedPasswordResetTokens : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PasswordResetTokens",
columns: table => new
{
TokenId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
ResetToken = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Created = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PasswordResetTokens", x => x.TokenId);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PasswordResetTokens");
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using LBPUnion.ProjectLighthouse;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220715222906_UserInvite")]
public partial class UserInvite : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "APIKeys",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Description = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Key = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Created = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Enabled = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_APIKeys", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "RegistrationTokens",
columns: table => new
{
TokenId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Token = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Created = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RegistrationTokens", x => x.TokenId);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "APIKeys");
migrationBuilder.DropTable(
name: "RegistrationTokens");
}
}
}

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
using LBPUnion.ProjectLighthouse;
using Microsoft.EntityFrameworkCore.Infrastructure;
#nullable disable
namespace ProjectLighthouse.Migrations
{
[DbContext(typeof(Database))]
[Migration("20220716234844_RemovedAPIKeyEnabled")]
public partial class RemovedAPIKeyEnabled : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Enabled",
table: "APIKeys");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Enabled",
table: "APIKeys",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -16,7 +16,7 @@ namespace ProjectLighthouse.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "6.0.5") .HasAnnotation("ProductVersion", "6.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b =>
@ -331,6 +331,26 @@ namespace ProjectLighthouse.Migrations
b.ToTable("VisitedLevels"); b.ToTable("VisitedLevels");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.APIKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<string>("Key")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("APIKeys");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.AuthenticationAttempt", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.AuthenticationAttempt", b =>
{ {
b.Property<int>("AuthenticationAttemptId") b.Property<int>("AuthenticationAttemptId")
@ -390,6 +410,26 @@ namespace ProjectLighthouse.Migrations
b.ToTable("GameTokens"); b.ToTable("GameTokens");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.PasswordResetToken", b =>
{
b.Property<int>("TokenId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("ResetToken")
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("TokenId");
b.ToTable("PasswordResetTokens");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Photo", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Photo", b =>
{ {
b.Property<int>("PhotoId") b.Property<int>("PhotoId")
@ -595,6 +635,9 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("AdminGrantedSlots") b.Property<int>("AdminGrantedSlots")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("ApprovedIPAddress")
.HasColumnType("longtext");
b.Property<bool>("Banned") b.Property<bool>("Banned")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@ -659,25 +702,6 @@ namespace ProjectLighthouse.Migrations
b.ToTable("Users"); b.ToTable("Users");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Profiles.UserApprovedIpAddress", b =>
{
b.Property<int>("UserApprovedIpAddressId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("IpAddress")
.HasColumnType("longtext");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("UserApprovedIpAddressId");
b.HasIndex("UserId");
b.ToTable("UserApprovedIpAddresses");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reaction", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reaction", b =>
{ {
b.Property<int>("RatingId") b.Property<int>("RatingId")
@ -698,6 +722,23 @@ namespace ProjectLighthouse.Migrations
b.ToTable("Reactions"); b.ToTable("Reactions");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.RegistrationToken", b =>
{
b.Property<int>("TokenId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("Created")
.HasColumnType("datetime(6)");
b.Property<string>("Token")
.HasColumnType("longtext");
b.HasKey("TokenId");
b.ToTable("RegistrationTokens");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reviews.RatedReview", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reviews.RatedReview", b =>
{ {
b.Property<int>("RatedReviewId") b.Property<int>("RatedReviewId")
@ -1035,17 +1076,6 @@ namespace ProjectLighthouse.Migrations
b.Navigation("Location"); b.Navigation("Location");
}); });
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Profiles.UserApprovedIpAddress", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reviews.RatedReview", b => modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Reviews.RatedReview", b =>
{ {
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Reviews.Review", "Review") b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Reviews.Review", "Review")

View file

@ -0,0 +1,19 @@

using System;
using System.ComponentModel.DataAnnotations;
namespace LBPUnion.ProjectLighthouse.PlayerData
{
public class APIKey
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
public string Key { get; set; }
public DateTime Created { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace LBPUnion.ProjectLighthouse.PlayerData;
public class PasswordResetToken
{
// ReSharper disable once UnusedMember.Global
[Key]
public int TokenId { get; set; }
public int UserId { get; set; }
public string ResetToken { get; set; }
public DateTime Created { get; set; }
}

View file

@ -143,6 +143,11 @@ public class User
[JsonIgnore] [JsonIgnore]
public string BannedReason { get; set; } public string BannedReason { get; set; }
#nullable enable
[JsonIgnore]
public string? ApprovedIPAddress { get; set; }
#nullable disable
public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1) public string Serialize(GameVersion gameVersion = GameVersion.LittleBigPlanet1)
{ {
string user = LbpSerializer.TaggedStringElement("npHandle", this.Username, "icon", this.IconHash) + string user = LbpSerializer.TaggedStringElement("npHandle", this.Username, "icon", this.IconHash) +

View file

@ -1,17 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LBPUnion.ProjectLighthouse.PlayerData.Profiles;
public class UserApprovedIpAddress
{
[Key]
public int UserApprovedIpAddressId { get; set; }
public int UserId { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public string IpAddress { get; set; }
}

View file

@ -0,0 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace LBPUnion.ProjectLighthouse.PlayerData
{
public class RegistrationToken
{
[Key]
public int TokenId { get; set; }
public string Token { get; set; }
public DateTime Created { get; set; }
}
}

View file

@ -12,19 +12,19 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="DDSReader" Version="1.0.8-pre" /> <PackageReference Include="DDSReader" Version="1.0.8-pre" />
<PackageReference Include="Discord.Net.Webhook" Version="3.7.2" /> <PackageReference Include="Discord.Net.Webhook" Version="3.7.2" />
<PackageReference Include="InfluxDB.Client" Version="4.2.0" /> <PackageReference Include="InfluxDB.Client" Version="4.3.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
<PackageReference Include="Redis.OM" Version="0.1.9" /> <PackageReference Include="Redis.OM" Version="0.2.1" />
<PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" /> <PackageReference Include="YamlDotNet" Version="12.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

View file

@ -27,6 +27,7 @@ canvas.hide-subjects {
.card { .card {
display: flex; display: flex;
overflow-wrap: anywhere;
width: 100%; width: 100%;
} }

View file

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Redis.OM.Searching; using Redis.OM.Searching;
@ -17,15 +18,15 @@ public class RedisStorableList<T> : StorableList<T>
this.redisNormalCollection.Insert(item); this.redisNormalCollection.Insert(item);
} }
public override Task RemoveAsync(T item) => this.redisNormalCollection.Delete(item); public override Task RemoveAsync(T item) => this.redisNormalCollection.DeleteAsync(item);
public override void Remove(T item) public override void Remove(T item)
{ {
this.redisNormalCollection.DeleteSync(item); this.redisNormalCollection.Delete(item);
} }
public override Task UpdateAsync(T item) => this.redisNormalCollection.Update(item); public override Task UpdateAsync(T item) => this.redisNormalCollection.UpdateAsync(item);
public override void Update(T item) public override void Update(T item)
{ {
this.redisNormalCollection.UpdateSync(item); this.redisNormalCollection.Update(item);
} }
} }

View file

@ -20,7 +20,7 @@ environment until release**.
This is because we have not entirely nailed security down yet, and **your instance WILL get attacked** as a result. It's This is because we have not entirely nailed security down yet, and **your instance WILL get attacked** as a result. It's
happened before, and it'll happen again. happened before, and it'll happen again.
Simply put, **Project Lighthouse is not ready for the general public yet**. Simply put, **Project Lighthouse is not ready for the public yet**.
In addition, we're not responsible if someone hacks your machine and wipes your database, so make frequent backups, and In addition, we're not responsible if someone hacks your machine and wipes your database, so make frequent backups, and
be sure to report any vulnerabilities. Thank you in advance. be sure to report any vulnerabilities. Thank you in advance.
@ -61,12 +61,12 @@ information.
## Compatibility across games and platforms ## Compatibility across games and platforms
| Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5/Vita) | | Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5/Adrenaline) |
|----------|-------------------------------------|--------------------------------------------------------------|-------------------------| |----------|------------------------|-------------------------------------------|-------------------------------|
| LBP1 | Compatible | Compatible with patched RPCS3 build | No next-gen equivalent | | LBP1 | Compatible | Compatible | No next-gen equivalent |
| LBP2 | Compatible | Compatible with patched RPCS3 build | No next-gen equivalent | | LBP2 | Compatible | Compatible | No next-gen equivalent |
| LBP3 | Mostly compatible, frequent crashes | Mostly compatible with patched RPCS3 build, frequent crashes | Incompatible | | LBP3 | Mostly compatible | Mostly compatible | Incompatible |
| LBP Vita | Compatible | Incompatible, marked as "Intro" on Vita3k | No next-gen equivalent | | LBP Vita | Compatible | Incompatible, PSN not supported on Vita3k | No next-gen equivalent |
| LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible | | LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible |
Project Lighthouse is mostly a work in progress, so this chart is subject to change at any point. Project Lighthouse is mostly a work in progress, so this chart is subject to change at any point.

View file

@ -1,3 +1,4 @@
#!/bin/sh
# Build script for production # Build script for production
# #
# No arguments # No arguments

View file

@ -1,3 +1,4 @@
#!/bin/sh
# Startup script for production # Startup script for production
# #
# $1: Server to start; case sensitive!!!!! # $1: Server to start; case sensitive!!!!!

View file

@ -0,0 +1,12 @@
#!/bin/sh
# Update script for production
#
# No arguments
# Called manually
sudo systemctl stop project-lighthouse*
cd /srv/lighthouse || return
sudo -u lighthouse -i /srv/lighthouse/build.sh
sudo systemctl start project-lighthouse*