mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-31 01:08:41 +00:00
Merge branch 'main' into more-slot-categories
This commit is contained in:
commit
71fa2c1617
26 changed files with 629 additions and 90 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
@ -58,15 +58,10 @@ jobs:
|
||||||
name: lighthouse-test-results-${{matrix.os.prettyName}}
|
name: lighthouse-test-results-${{matrix.os.prettyName}}
|
||||||
path: ${{github.workspace}}/TestResults-${{matrix.os.prettyName}}.trx
|
path: ${{github.workspace}}/TestResults-${{matrix.os.prettyName}}.trx
|
||||||
|
|
||||||
- name: Process Test Results (Control)
|
|
||||||
if: ${{ matrix.os.prettyName == 'Linux' }}
|
|
||||||
uses: im-open/process-dotnet-test-results@v2.0.1
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Process Test Results
|
- name: Process Test Results
|
||||||
if: ${{ matrix.os.prettyName != 'Linux' }}
|
id: process-trx
|
||||||
uses: im-open/process-dotnet-test-results@v2.0.1
|
if: ${{ always() }}
|
||||||
|
uses: im-open/process-dotnet-test-results@v2.0.2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
create-status-check: false
|
create-status-check: false
|
||||||
|
|
|
@ -4,9 +4,11 @@ using Xunit;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Tests
|
namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
{
|
{
|
||||||
public sealed class DatabaseFact : FactAttribute
|
public sealed class DatabaseFactAttribute : FactAttribute
|
||||||
{
|
{
|
||||||
public DatabaseFact()
|
private static readonly object migrateLock = new();
|
||||||
|
|
||||||
|
public DatabaseFactAttribute()
|
||||||
{
|
{
|
||||||
ServerSettings.Instance = new ServerSettings();
|
ServerSettings.Instance = new ServerSettings();
|
||||||
ServerSettings.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
|
ServerSettings.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse;database=lighthouse";
|
||||||
|
@ -16,8 +18,11 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
using Database database = new();
|
lock(migrateLock)
|
||||||
database.Database.Migrate();
|
{
|
||||||
|
using Database database = new();
|
||||||
|
database.Database.Migrate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using LBPUnion.ProjectLighthouse.Helpers;
|
using LBPUnion.ProjectLighthouse.Helpers;
|
||||||
|
@ -25,9 +25,13 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
|
|
||||||
this.Client = this.Server.CreateClient();
|
this.Client = this.Server.CreateClient();
|
||||||
}
|
}
|
||||||
|
public async Task<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)
|
||||||
public async Task<HttpResponseMessage> AuthenticateResponse(int number = 0, bool createUser = true)
|
|
||||||
{
|
{
|
||||||
|
if (number == -1)
|
||||||
|
{
|
||||||
|
number = new Random().Next();
|
||||||
|
}
|
||||||
|
|
||||||
const string username = "unitTestUser";
|
const string username = "unitTestUser";
|
||||||
if (createUser)
|
if (createUser)
|
||||||
{
|
{
|
||||||
|
@ -65,8 +69,8 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> UploadFileEndpointRequest(string filePath)
|
public async Task<HttpResponseMessage> UploadFileEndpointRequest(string filePath)
|
||||||
{
|
{
|
||||||
byte[] bytes = Encoding.UTF8.GetBytes(await File.ReadAllTextAsync(filePath));
|
byte[] bytes = await File.ReadAllBytesAsync(filePath);
|
||||||
string hash = HashHelper.Sha1Hash(bytes);
|
string hash = HashHelper.Sha1Hash(bytes).ToLower();
|
||||||
|
|
||||||
return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes));
|
return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -18,6 +19,7 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
public async Task ShouldNotAcceptScript()
|
public async Task ShouldNotAcceptScript()
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestScript.ff");
|
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestScript.ff");
|
||||||
|
Assert.False(response.StatusCode == HttpStatusCode.Forbidden);
|
||||||
Assert.False(response.IsSuccessStatusCode);
|
Assert.False(response.IsSuccessStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +27,7 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
public async Task ShouldNotAcceptFarc()
|
public async Task ShouldNotAcceptFarc()
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestFarc.farc");
|
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestFarc.farc");
|
||||||
|
Assert.False(response.StatusCode == HttpStatusCode.Forbidden);
|
||||||
Assert.False(response.IsSuccessStatusCode);
|
Assert.False(response.IsSuccessStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
public async Task ShouldNotAcceptGarbage()
|
public async Task ShouldNotAcceptGarbage()
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestGarbage.bin");
|
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestGarbage.bin");
|
||||||
|
Assert.False(response.StatusCode == HttpStatusCode.Forbidden);
|
||||||
Assert.False(response.IsSuccessStatusCode);
|
Assert.False(response.IsSuccessStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
public async Task ShouldAcceptTexture()
|
public async Task ShouldAcceptTexture()
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestTexture.tex");
|
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestTexture.tex");
|
||||||
|
Assert.False(response.StatusCode == HttpStatusCode.Forbidden);
|
||||||
Assert.True(response.IsSuccessStatusCode);
|
Assert.True(response.IsSuccessStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +51,7 @@ namespace LBPUnion.ProjectLighthouse.Tests
|
||||||
public async Task ShouldAcceptLevel()
|
public async Task ShouldAcceptLevel()
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestLevel.lvl");
|
HttpResponseMessage response = await this.UploadFileEndpointRequest("ExampleFiles/TestLevel.lvl");
|
||||||
|
Assert.False(response.StatusCode == HttpStatusCode.Forbidden);
|
||||||
Assert.True(response.IsSuccessStatusCode);
|
Assert.True(response.IsSuccessStatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,41 +43,59 @@ namespace LBPUnion.ProjectLighthouse.Controllers
|
||||||
}
|
}
|
||||||
if (loginData == null) return this.BadRequest();
|
if (loginData == null) return this.BadRequest();
|
||||||
|
|
||||||
IPAddress? ipAddress = this.HttpContext.Connection.RemoteIpAddress;
|
IPAddress? remoteIpAddress = this.HttpContext.Connection.RemoteIpAddress;
|
||||||
if (ipAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever
|
if (remoteIpAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever
|
||||||
|
|
||||||
string userLocation = ipAddress.ToString();
|
string ipAddress = remoteIpAddress.ToString();
|
||||||
|
|
||||||
GameToken? token = await this.database.AuthenticateUser(loginData, userLocation, titleId);
|
// Get an existing token from the IP & username
|
||||||
if (token == null) return this.StatusCode(403, "");
|
GameToken? token = await this.database.GameTokens.Include
|
||||||
|
(t => t.User)
|
||||||
|
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == loginData.Username && !t.Used);
|
||||||
|
|
||||||
|
if (token == null) // If we cant find an existing token, try to generate a new one
|
||||||
|
{
|
||||||
|
token = await this.database.AuthenticateUser(loginData, ipAddress, titleId);
|
||||||
|
if (token == null) return this.StatusCode(403, ""); // If not, then 403.
|
||||||
|
}
|
||||||
|
|
||||||
User? user = await this.database.UserFromGameToken(token, true);
|
User? user = await this.database.UserFromGameToken(token, true);
|
||||||
if (user == null) return this.StatusCode(403, "");
|
if (user == null) return this.StatusCode(403, "");
|
||||||
|
|
||||||
if (ServerSettings.Instance.UseExternalAuth)
|
if (ServerSettings.Instance.UseExternalAuth)
|
||||||
{
|
{
|
||||||
string ipAddressAndName = $"{token.UserLocation}|{user.Username}";
|
if (ServerSettings.Instance.BlockDeniedUsers)
|
||||||
if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3)
|
|
||||||
{
|
{
|
||||||
this.database.AuthenticationAttempts.RemoveRange
|
string ipAddressAndName = $"{token.UserLocation}|{user.Username}";
|
||||||
(this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId));
|
if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3)
|
||||||
|
{
|
||||||
|
this.database.AuthenticationAttempts.RemoveRange
|
||||||
|
(this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId));
|
||||||
|
|
||||||
DeniedAuthenticationHelper.AddAttempt(ipAddressAndName);
|
DeniedAuthenticationHelper.AddAttempt(ipAddressAndName);
|
||||||
|
|
||||||
await this.database.SaveChangesAsync();
|
await this.database.SaveChangesAsync();
|
||||||
return this.StatusCode(403, "");
|
return this.StatusCode(403, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationAttempt authAttempt = new()
|
if (this.database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).Select(a => a.IpAddress).Contains(ipAddress))
|
||||||
{
|
{
|
||||||
GameToken = token,
|
token.Approved = true;
|
||||||
GameTokenId = token.TokenId,
|
}
|
||||||
Timestamp = TimestampHelper.Timestamp,
|
else
|
||||||
IPAddress = userLocation,
|
{
|
||||||
Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3
|
AuthenticationAttempt authAttempt = new()
|
||||||
};
|
{
|
||||||
|
GameToken = token,
|
||||||
|
GameTokenId = token.TokenId,
|
||||||
|
Timestamp = TimestampHelper.Timestamp,
|
||||||
|
IPAddress = ipAddress,
|
||||||
|
Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3
|
||||||
|
};
|
||||||
|
|
||||||
this.database.AuthenticationAttempts.Add(authAttempt);
|
this.database.AuthenticationAttempts.Add(authAttempt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -86,9 +104,18 @@ namespace LBPUnion.ProjectLighthouse.Controllers
|
||||||
|
|
||||||
await this.database.SaveChangesAsync();
|
await this.database.SaveChangesAsync();
|
||||||
|
|
||||||
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance);
|
if (!token.Approved) return this.StatusCode(403, "");
|
||||||
|
|
||||||
// Create a new room on LBP2+/Vita
|
Logger.Log($"Successfully logged in user {user.Username} as {token.GameVersion} client ({titleId})", LoggerLevelLogin.Instance);
|
||||||
|
// After this point we are now considering this session as logged in.
|
||||||
|
|
||||||
|
// We just logged in with the token. Mark it as used so someone else doesnt try to use it,
|
||||||
|
// and so we don't pick the same token up when logging in later.
|
||||||
|
token.Used = true;
|
||||||
|
|
||||||
|
await this.database.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Create a new room on LBP2/3/Vita
|
||||||
if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user);
|
if (token.GameVersion != GameVersion.LittleBigPlanet1) RoomHelper.CreateRoom(user);
|
||||||
|
|
||||||
return this.Ok
|
return this.Ok
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#nullable enable
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kettu;
|
using Kettu;
|
||||||
|
@ -22,25 +23,44 @@ namespace LBPUnion.ProjectLighthouse.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("eula")]
|
[HttpGet("eula")]
|
||||||
public IActionResult Eula() => this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n");
|
public async Task<IActionResult> Eula()
|
||||||
|
{
|
||||||
|
User? user = await this.database.UserFromGameRequest(this.Request);
|
||||||
|
if (user == null) return this.StatusCode(403, "");
|
||||||
|
|
||||||
|
return this.Ok(ServerSettings.Instance.EulaText + "\n" + $"{EulaHelper.License}\n");
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("announce")]
|
[HttpGet("announce")]
|
||||||
public async Task<IActionResult> Announce()
|
public async Task<IActionResult> Announce()
|
||||||
{
|
{
|
||||||
User user = await this.database.UserFromGameRequest(this.Request, true);
|
#if !DEBUG
|
||||||
|
User? user = await this.database.UserFromGameRequest(this.Request);
|
||||||
if (user == null) return this.StatusCode(403, "");
|
if (user == null) return this.StatusCode(403, "");
|
||||||
|
#else
|
||||||
|
(User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request);
|
||||||
|
|
||||||
if (ServerSettings.Instance.UseExternalAuth)
|
if (userAndToken == null) return this.StatusCode(403, "");
|
||||||
return this.Ok
|
|
||||||
(
|
|
||||||
"Please stay on this screen.\n" +
|
|
||||||
$"Before continuing, you must approve this session at {ServerSettings.Instance.ExternalUrl}.\n" +
|
|
||||||
"Please keep in mind that if the session is denied you may have to wait up to 5-10 minutes to try logging in again.\n" +
|
|
||||||
"Once approved, you may press X and continue.\n\n" +
|
|
||||||
ServerSettings.Instance.EulaText
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.Ok($"You are now logged in as {user.Username} (id: {user.UserId}).\n\n" + ServerSettings.Instance.EulaText);
|
// ReSharper disable once PossibleInvalidOperationException
|
||||||
|
User user = userAndToken.Value.Item1;
|
||||||
|
GameToken gameToken = userAndToken.Value.Item2;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return this.Ok
|
||||||
|
(
|
||||||
|
$"You are now logged in as {user.Username}.\n\n" +
|
||||||
|
#if DEBUG
|
||||||
|
"---DEBUG INFO---\n" +
|
||||||
|
$"user.UserId: {user.UserId}\n" +
|
||||||
|
$"token.Approved: {gameToken.Approved}\n" +
|
||||||
|
$"token.Used: {gameToken.Used}\n" +
|
||||||
|
$"token.UserLocation: {gameToken.UserLocation}\n" +
|
||||||
|
$"token.GameVersion: {gameToken.GameVersion}\n" +
|
||||||
|
"---DEBUG INFO---\n\n" +
|
||||||
|
#endif
|
||||||
|
ServerSettings.Instance.EulaText
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("notification")]
|
[HttpGet("notification")]
|
||||||
|
@ -52,7 +72,7 @@ namespace LBPUnion.ProjectLighthouse.Controllers
|
||||||
[HttpPost("filter")]
|
[HttpPost("filter")]
|
||||||
public async Task<IActionResult> Filter()
|
public async Task<IActionResult> Filter()
|
||||||
{
|
{
|
||||||
User user = await this.database.UserFromGameRequest(this.Request);
|
User? user = await this.database.UserFromGameRequest(this.Request);
|
||||||
if (user == null) return this.StatusCode(403, "");
|
if (user == null) return this.StatusCode(403, "");
|
||||||
|
|
||||||
string loggedText = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
string loggedText = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Controllers.Website.ExternalAuth
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("/authentication")]
|
||||||
|
public class AutoApprovalController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly Database database;
|
||||||
|
|
||||||
|
public AutoApprovalController(Database database)
|
||||||
|
{
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("autoApprove/{id:int}")]
|
||||||
|
public async Task<IActionResult> AutoApprove([FromRoute] int id)
|
||||||
|
{
|
||||||
|
User? user = this.database.UserFromWebRequest(this.Request);
|
||||||
|
if (user == null) return this.Redirect("/login");
|
||||||
|
|
||||||
|
AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include
|
||||||
|
(a => a.GameToken)
|
||||||
|
.FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id);
|
||||||
|
|
||||||
|
if (authAttempt == null) return this.BadRequest();
|
||||||
|
if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login");
|
||||||
|
|
||||||
|
authAttempt.GameToken.Approved = true;
|
||||||
|
|
||||||
|
UserApprovedIpAddress approvedIpAddress = new()
|
||||||
|
{
|
||||||
|
UserId = user.UserId,
|
||||||
|
User = user,
|
||||||
|
IpAddress = authAttempt.IPAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.database.UserApprovedIpAddresses.Add(approvedIpAddress);
|
||||||
|
this.database.AuthenticationAttempts.Remove(authAttempt);
|
||||||
|
|
||||||
|
await this.database.SaveChangesAsync();
|
||||||
|
|
||||||
|
return this.Redirect("/authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("revokeAutoApproval/{id:int}")]
|
||||||
|
public async Task<IActionResult> RevokeAutoApproval([FromRoute] int id)
|
||||||
|
{
|
||||||
|
User? user = this.database.UserFromWebRequest(this.Request);
|
||||||
|
if (user == null) return this.Redirect("/login");
|
||||||
|
|
||||||
|
UserApprovedIpAddress? approvedIpAddress = await this.database.UserApprovedIpAddresses.FirstOrDefaultAsync(a => a.UserApprovedIpAddressId == id);
|
||||||
|
if (approvedIpAddress == null) return this.BadRequest();
|
||||||
|
if (approvedIpAddress.UserId != user.UserId) return this.Redirect("/login");
|
||||||
|
|
||||||
|
this.database.UserApprovedIpAddresses.Remove(approvedIpAddress);
|
||||||
|
|
||||||
|
await this.database.SaveChangesAsync();
|
||||||
|
|
||||||
|
return this.Redirect("/authentication/autoApprovals");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ namespace LBPUnion.ProjectLighthouse
|
||||||
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; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
=> options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
|
=> options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
|
||||||
|
@ -66,13 +67,13 @@ namespace LBPUnion.ProjectLighthouse
|
||||||
#nullable enable
|
#nullable enable
|
||||||
public async Task<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "")
|
public async Task<GameToken?> AuthenticateUser(LoginData loginData, string userLocation, string titleId = "")
|
||||||
{
|
{
|
||||||
// TODO: don't use psn name to authenticate
|
|
||||||
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username);
|
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username);
|
||||||
if (user == null) return null;
|
if (user == null) return null;
|
||||||
|
|
||||||
GameToken gameToken = new()
|
GameToken gameToken = new()
|
||||||
{
|
{
|
||||||
UserToken = HashHelper.GenerateAuthToken(),
|
UserToken = HashHelper.GenerateAuthToken(),
|
||||||
|
User = user,
|
||||||
UserId = user.UserId,
|
UserId = user.UserId,
|
||||||
UserLocation = userLocation,
|
UserLocation = userLocation,
|
||||||
GameVersion = GameVersionHelper.FromTitleId(titleId),
|
GameVersion = GameVersionHelper.FromTitleId(titleId),
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
using LBPUnion.ProjectLighthouse;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ProjectLighthouse.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(Database))]
|
||||||
|
[Migration("20211213195540_AddUserApprovedIpAddresses")]
|
||||||
|
public partial class AddUserApprovedIpAddresses : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserApprovedIpAddresses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
using LBPUnion.ProjectLighthouse;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ProjectLighthouse.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(Database))]
|
||||||
|
[Migration("20211214005427_AddUsedBoolToGameToken")]
|
||||||
|
public partial class AddUsedBoolToGameToken : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Incompatible with old tokens
|
||||||
|
migrationBuilder.Sql("DELETE FROM AuthenticationAttempts");
|
||||||
|
migrationBuilder.Sql("DELETE FROM GameTokens");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Used",
|
||||||
|
table: "GameTokens",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_GameTokens_UserId",
|
||||||
|
table: "GameTokens",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_GameTokens_Users_UserId",
|
||||||
|
table: "GameTokens",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_GameTokens_Users_UserId",
|
||||||
|
table: "GameTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_GameTokens_UserId",
|
||||||
|
table: "GameTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Used",
|
||||||
|
table: "GameTokens");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,9 @@ namespace ProjectLighthouse.Migrations
|
||||||
b.Property<int>("GameVersion")
|
b.Property<int>("GameVersion")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("Used")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
b.Property<int>("UserId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
@ -66,6 +69,8 @@ namespace ProjectLighthouse.Migrations
|
||||||
|
|
||||||
b.HasKey("TokenId");
|
b.HasKey("TokenId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("GameTokens");
|
b.ToTable("GameTokens");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -587,6 +592,25 @@ namespace ProjectLighthouse.Migrations
|
||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.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.Types.WebToken", b =>
|
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.WebToken", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("TokenId")
|
b.Property<int>("TokenId")
|
||||||
|
@ -615,6 +639,17 @@ namespace ProjectLighthouse.Migrations
|
||||||
b.Navigation("GameToken");
|
b.Navigation("GameToken");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.GameToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.HeartedProfile", b =>
|
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.HeartedProfile", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "HeartedUser")
|
b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "HeartedUser")
|
||||||
|
@ -829,6 +864,17 @@ namespace ProjectLighthouse.Migrations
|
||||||
|
|
||||||
b.Navigation("Location");
|
b.Navigation("Location");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.UserApprovedIpAddress", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
@{
|
@{
|
||||||
Layout = "Layouts/BaseLayout";
|
Layout = "Layouts/BaseLayout";
|
||||||
|
Model.Title = "Authentication";
|
||||||
}
|
}
|
||||||
<h1>Authentication</h1>
|
|
||||||
|
|
||||||
@if (Model.AuthenticationAttempts.Count == 0)
|
@if (Model.AuthenticationAttempts.Count == 0)
|
||||||
{
|
{
|
||||||
|
@ -14,22 +14,48 @@
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
|
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
|
||||||
<a href="/authentication/denyAll">
|
@if (Model.IpAddress != null)
|
||||||
<button class="ui small red button">Deny all</button>
|
{
|
||||||
</a>
|
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with an authentication attempt below, then it's safe to assume the authentication attempt came from the same network as this device.</p>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<a href="/authentication/autoApprovals">
|
||||||
|
<button class="ui small blue button">
|
||||||
|
<i class="cog icon"></i>
|
||||||
|
<span>Manage automatically approved IP addresses</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/authentication/denyAll">
|
||||||
|
<button class="ui small 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)
|
||||||
{
|
{
|
||||||
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp);
|
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp);
|
||||||
<div class="ui red segment">
|
<div class="ui red segment">
|
||||||
<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">
|
||||||
|
<button class="ui tiny green button">
|
||||||
|
<i class="check icon"></i>
|
||||||
|
<span>Automatically approve every time</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
|
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
|
||||||
<button class="ui tiny green button">Approve</button>
|
<button class="ui tiny yellow button">
|
||||||
|
<i class="check icon"></i>
|
||||||
|
<span>Approve this time</span>
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
|
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
|
||||||
<button class="ui tiny red button">Deny</button>
|
<button class="ui tiny red button">
|
||||||
|
<i class="x icon"></i>
|
||||||
|
<span>Deny</span>
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LBPUnion.ProjectLighthouse.Pages.Layouts;
|
using LBPUnion.ProjectLighthouse.Pages.Layouts;
|
||||||
using LBPUnion.ProjectLighthouse.Types;
|
using LBPUnion.ProjectLighthouse.Types;
|
||||||
|
@ -13,12 +15,17 @@ namespace LBPUnion.ProjectLighthouse.Pages.ExternalAuth
|
||||||
{
|
{
|
||||||
|
|
||||||
public List<AuthenticationAttempt> AuthenticationAttempts;
|
public List<AuthenticationAttempt> AuthenticationAttempts;
|
||||||
|
|
||||||
|
public IPAddress? IpAddress;
|
||||||
public AuthenticationPage(Database database) : base(database)
|
public AuthenticationPage(Database database) : base(database)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
public async Task<IActionResult> OnGet()
|
public async Task<IActionResult> OnGet()
|
||||||
{
|
{
|
||||||
if (!ServerSettings.Instance.UseExternalAuth) return this.NotFound();
|
if (!ServerSettings.Instance.UseExternalAuth) return this.NotFound();
|
||||||
|
if (this.User == null) return this.StatusCode(403, "");
|
||||||
|
|
||||||
|
this.IpAddress = this.HttpContext.Connection.RemoteIpAddress;
|
||||||
|
|
||||||
this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include
|
this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include
|
||||||
(a => a.GameToken)
|
(a => a.GameToken)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
@page "/authentication/autoApprovals"
|
||||||
|
@using LBPUnion.ProjectLighthouse.Types
|
||||||
|
@model LBPUnion.ProjectLighthouse.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>
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LBPUnion.ProjectLighthouse.Pages.Layouts;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Pages.ExternalAuth
|
||||||
|
{
|
||||||
|
public class ManageUserApprovedIpAddressesPage : BaseLayout
|
||||||
|
{
|
||||||
|
public ManageUserApprovedIpAddressesPage(Database database) : base(database)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public List<UserApprovedIpAddress> ApprovedIpAddresses;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
@using LBPUnion.ProjectLighthouse.Types
|
@using LBPUnion.ProjectLighthouse.Types
|
||||||
|
@using LBPUnion.ProjectLighthouse.Types.Settings
|
||||||
@model LBPUnion.ProjectLighthouse.Pages.LandingPage
|
@model LBPUnion.ProjectLighthouse.Pages.LandingPage
|
||||||
|
|
||||||
@{
|
@{
|
||||||
|
@ -11,6 +12,12 @@
|
||||||
@if (Model.User != null)
|
@if (Model.User != null)
|
||||||
{
|
{
|
||||||
<p>You are currently logged in as <b>@Model.User.Username</b>.</p>
|
<p>You are currently logged in as <b>@Model.User.Username</b>.</p>
|
||||||
|
if (ServerSettings.Instance.UseExternalAuth && Model.AuthenticationAttemptsCount > 0)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<b>You have @Model.AuthenticationAttemptsCount authentication attempts pending. Click <a href="/authentication">here</a> to view them.</b>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (Model.PlayersOnlineCount == 1)
|
@if (Model.PlayersOnlineCount == 1)
|
||||||
|
@ -21,6 +28,7 @@
|
||||||
<a href="/user/@user.UserId">@user.Username</a>
|
<a href="/user/@user.UserId">@user.Username</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (Model.PlayersOnlineCount == 0)
|
else if (Model.PlayersOnlineCount == 0)
|
||||||
{
|
{
|
||||||
<p>There are no users online. Why not hop on?</p>
|
<p>There are no users online. Why not hop on?</p>
|
||||||
|
|
|
@ -16,6 +16,8 @@ namespace LBPUnion.ProjectLighthouse.Pages
|
||||||
public List<User> PlayersOnline;
|
public List<User> PlayersOnline;
|
||||||
|
|
||||||
public int PlayersOnlineCount;
|
public int PlayersOnlineCount;
|
||||||
|
|
||||||
|
public int AuthenticationAttemptsCount;
|
||||||
public LandingPage(Database database) : base(database)
|
public LandingPage(Database database) : base(database)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -27,6 +29,13 @@ namespace LBPUnion.ProjectLighthouse.Pages
|
||||||
|
|
||||||
this.PlayersOnlineCount = await StatisticsHelper.RecentMatches();
|
this.PlayersOnlineCount = await StatisticsHelper.RecentMatches();
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
this.AuthenticationAttemptsCount = await this.Database.AuthenticationAttempts.Include
|
||||||
|
(a => a.GameToken)
|
||||||
|
.CountAsync(a => a.GameToken.UserId == user.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
List<int> userIds = await this.Database.LastContacts.Where(l => TimestampHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
|
List<int> userIds = await this.Database.LastContacts.Where(l => TimestampHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
|
||||||
|
|
||||||
this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync();
|
this.PlayersOnline = await this.Database.Users.Where(u => userIds.Contains(u.UserId)).ToListAsync();
|
||||||
|
|
|
@ -64,6 +64,17 @@
|
||||||
gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId');
|
gtag('config', '@ServerSettings.Instance.GoogleAnalyticsId');
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas.photo-subjects {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.hide-subjects {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="pageContainer">
|
<div class="pageContainer">
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
@using LBPUnion.ProjectLighthouse.Types
|
@using LBPUnion.ProjectLighthouse.Types
|
||||||
@model LBPUnion.ProjectLighthouse.Types.Photo
|
@model LBPUnion.ProjectLighthouse.Types.Photo
|
||||||
|
|
||||||
<img src="/gameAssets/@Model.LargeHash" style="width: 100%; height: auto; border-radius: .28571429rem;">
|
|
||||||
|
<div style="position: relative">
|
||||||
|
<canvas class="hide-subjects" id="canvas-subjects-@Model.PhotoId" width="1920" height="1080"
|
||||||
|
style="position: absolute; transform: rotate(180deg)">
|
||||||
|
</canvas>
|
||||||
|
<img id="game-image-@Model.PhotoId" src="/gameAssets/@Model.LargeHash"
|
||||||
|
style="width: 100%; height: auto; border-radius: .28571429rem;">
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -16,7 +23,104 @@
|
||||||
<p>
|
<p>
|
||||||
<b>Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):</b>
|
<b>Photo contains @Model.Subjects.Count @(Model.Subjects.Count == 1 ? "person" : "people"):</b>
|
||||||
</p>
|
</p>
|
||||||
@foreach (PhotoSubject subject in Model.Subjects)
|
<div id="hover-subjects-@Model.PhotoId">
|
||||||
{
|
@foreach (PhotoSubject subject in Model.Subjects)
|
||||||
<a href="/user/@subject.UserId">@subject.User.Username</a>
|
{
|
||||||
}
|
<a href="/user/@subject.UserId">@subject.User.Username</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@{
|
||||||
|
PhotoSubject[] subjects = Model.Subjects.ToArray();
|
||||||
|
foreach (PhotoSubject subject in subjects) subject.Username = subject.User.Username;
|
||||||
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// render the page first so that image heights have been calculated
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
const canvas = document.getElementById("canvas-subjects-@Model.PhotoId");
|
||||||
|
const hoverer = document.getElementById("hover-subjects-@Model.PhotoId");
|
||||||
|
const image = document.getElementById("game-image-@Model.PhotoId");
|
||||||
|
|
||||||
|
hoverer.addEventListener('mouseenter', function () {
|
||||||
|
canvas.className = "photo-subjects";
|
||||||
|
});
|
||||||
|
hoverer.addEventListener('mouseleave', function () {
|
||||||
|
canvas.className = "photo-subjects hide-subjects";
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const subjects = @Html.Raw(Json.Serialize(subjects.ToArray()));
|
||||||
|
|
||||||
|
canvas.width = image.offsetWidth;
|
||||||
|
canvas.height = image.clientHeight; // space for names to hang off
|
||||||
|
|
||||||
|
const w = canvas.width;
|
||||||
|
const h = canvas.height;
|
||||||
|
|
||||||
|
// halfwidth, halfheight
|
||||||
|
const hw = w / 2;
|
||||||
|
const hh = h / 2;
|
||||||
|
|
||||||
|
const colours = ["#96dd3c", "#ceb424", "#cc0a1d", "#c800cc"];
|
||||||
|
|
||||||
|
subjects.forEach((s, si) => {
|
||||||
|
const colour = colours[si % 4];
|
||||||
|
|
||||||
|
// Bounding box
|
||||||
|
const bounds = s.bounds.split(",").map(parseFloat);
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2] = bounds.map(n => Math.min(Math.max(n, -1), 1));
|
||||||
|
|
||||||
|
const bx = hw - (x2 * hw);
|
||||||
|
const by = hh - (y2 * hh);
|
||||||
|
const bw = (x2 - x1) * hw;
|
||||||
|
const bh = (y2 - y1) * hh;
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.lineWidth = 3;
|
||||||
|
context.strokeStyle = colour;
|
||||||
|
context.rect(bx, by, bw, bh);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
// Move into relative coordinates from bounding box
|
||||||
|
context.translate(bx, by);
|
||||||
|
|
||||||
|
// Username label
|
||||||
|
context.font = "16px Lato";
|
||||||
|
context.fillStyle = colour;
|
||||||
|
|
||||||
|
// Text width/height for the label background
|
||||||
|
const tw = context.measureText(s.username).width;
|
||||||
|
const th = 24;
|
||||||
|
|
||||||
|
// Check if the label will flow off the bottom of the frame
|
||||||
|
const overflowBottom = (bounds[3] * hh) > (hh - 24);
|
||||||
|
// Check if the label will flow off the left of the frame
|
||||||
|
const overflowLeft = (bounds[2] * hw - tw) < (-hw);
|
||||||
|
|
||||||
|
// Set alignment
|
||||||
|
context.textAlign = overflowLeft ? "start" : "end";
|
||||||
|
|
||||||
|
// Text x / y
|
||||||
|
const lx = overflowLeft ? -bw + 6 : -6;
|
||||||
|
const ly = overflowBottom ? -bh - 6 : 16;
|
||||||
|
|
||||||
|
// Label background x / y
|
||||||
|
const lbx = overflowLeft ? bw - tw - 12 : -2;
|
||||||
|
const lby = overflowBottom ? bh : -24;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
context.fillRect(lbx, lby, tw + 16, th);
|
||||||
|
|
||||||
|
// Draw text, rotated back upright (canvas draws rotated 180deg)
|
||||||
|
context.fillStyle = "white";
|
||||||
|
context.rotate(Math.PI);
|
||||||
|
context.fillText(s.username, lx, ly);
|
||||||
|
|
||||||
|
// reset transform
|
||||||
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
})
|
||||||
|
}, false);
|
||||||
|
</script>
|
|
@ -1,4 +1,6 @@
|
||||||
@page "/user/{userId:int}"
|
@page "/user/{userId:int}"
|
||||||
|
@using System.IO
|
||||||
|
@using System.Web
|
||||||
@using LBPUnion.ProjectLighthouse.Types
|
@using LBPUnion.ProjectLighthouse.Types
|
||||||
@using LBPUnion.ProjectLighthouse.Types.Profiles
|
@using LBPUnion.ProjectLighthouse.Types.Profiles
|
||||||
@using LBPUnion.ProjectLighthouse.Types.Settings
|
@using LBPUnion.ProjectLighthouse.Types.Settings
|
||||||
|
@ -74,7 +76,7 @@
|
||||||
@foreach (Photo photo in Model.Photos)
|
@foreach (Photo photo in Model.Photos)
|
||||||
{
|
{
|
||||||
<div class="eight wide column">
|
<div class="eight wide column">
|
||||||
@await Html.PartialAsync("Partials/PhotoPartial", photo);
|
@await Html.PartialAsync("Partials/PhotoPartial", photo)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,9 +94,12 @@
|
||||||
@foreach (Comment comment in Model.Comments!)
|
@foreach (Comment comment in Model.Comments!)
|
||||||
{
|
{
|
||||||
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
|
DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000);
|
||||||
|
StringWriter messageWriter = new StringWriter();
|
||||||
|
HttpUtility.HtmlDecode(comment.Message, messageWriter);
|
||||||
|
String decodedMessage = messageWriter.ToString();
|
||||||
<div>
|
<div>
|
||||||
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
|
<b><a href="/user/@comment.PosterUserId">@comment.Poster.Username</a>: </b>
|
||||||
<span>@comment.Message</span>
|
<span>@decodedMessage</span>
|
||||||
<p>
|
<p>
|
||||||
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
|
<i>@timestamp.ToString("MM/dd/yyyy @ h:mm tt") UTC</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -33,10 +33,6 @@
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<_ContentIncludedByDefault Remove="Pages\Admin\Index.cshtml"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||||
<Exec Command="git describe --long --always --dirty --exclude=\* --abbrev=8 > "$(ProjectDir)/gitVersion.txt""/>
|
<Exec Command="git describe --long --always --dirty --exclude=\* --abbrev=8 > "$(ProjectDir)/gitVersion.txt""/>
|
||||||
<Exec Command="git branch --show-current > "$(ProjectDir)/gitBranch.txt""/>
|
<Exec Command="git branch --show-current > "$(ProjectDir)/gitBranch.txt""/>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Types
|
namespace LBPUnion.ProjectLighthouse.Types
|
||||||
{
|
{
|
||||||
|
@ -10,12 +11,19 @@ namespace LBPUnion.ProjectLighthouse.Types
|
||||||
|
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey(nameof(UserId))]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
public string UserToken { get; set; }
|
public string UserToken { get; set; }
|
||||||
|
|
||||||
public string UserLocation { get; set; }
|
public string UserLocation { get; set; }
|
||||||
|
|
||||||
public GameVersion GameVersion { get; set; }
|
public GameVersion GameVersion { get; set; }
|
||||||
|
|
||||||
public bool Approved { get; set; } = false;
|
// Set by /authentication webpage
|
||||||
|
public bool Approved { get; set; }
|
||||||
|
|
||||||
|
// Set to true on login
|
||||||
|
public bool Used { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using LBPUnion.ProjectLighthouse.Serialization;
|
using LBPUnion.ProjectLighthouse.Serialization;
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ namespace LBPUnion.ProjectLighthouse.Types
|
||||||
|
|
||||||
[XmlIgnore]
|
[XmlIgnore]
|
||||||
[ForeignKey(nameof(UserId))]
|
[ForeignKey(nameof(UserId))]
|
||||||
|
[JsonIgnore]
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
|
|
|
@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings
|
||||||
public class ServerSettings
|
public class ServerSettings
|
||||||
{
|
{
|
||||||
|
|
||||||
public const int CurrentConfigVersion = 12; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
|
public const int CurrentConfigVersion = 13; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE!
|
||||||
static ServerSettings()
|
static ServerSettings()
|
||||||
{
|
{
|
||||||
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
|
||||||
|
@ -97,6 +97,8 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings
|
||||||
|
|
||||||
public string GoogleAnalyticsId { get; set; } = "";
|
public string GoogleAnalyticsId { get; set; } = "";
|
||||||
|
|
||||||
|
public bool BlockDeniedUsers = true;
|
||||||
|
|
||||||
#region Meta
|
#region Meta
|
||||||
|
|
||||||
[NotNull]
|
[NotNull]
|
||||||
|
|
18
ProjectLighthouse/Types/UserApprovedIpAddress.cs
Normal file
18
ProjectLighthouse/Types/UserApprovedIpAddress.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Types
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
52
README.md
52
README.md
|
@ -5,15 +5,18 @@ This project is the main server component that LittleBigPlanet games connect to.
|
||||||
|
|
||||||
## WARNING!
|
## WARNING!
|
||||||
|
|
||||||
This is beta software, and thus is not ready for public use yet. We're not responsible if someone connects and hacks
|
This is **beta software**, and thus is **not stable**.
|
||||||
your entire machine and deletes all your files.
|
|
||||||
|
|
||||||
That said, feel free to develop privately!
|
We're not responsible if someone hacks your machine and wipes your database.
|
||||||
|
|
||||||
|
Make frequent backups, and be sure to report any vulnerabilities.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
This will be written when we're out of beta. Consider this your barrier to entry ;).
|
This will be written when we're out of beta. Consider this your barrier to entry ;).
|
||||||
|
|
||||||
|
It is recommended to build with Release if you plan to use Lighthouse in a production environment.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
Lighthouse requires a MySQL database at this time. For Linux users running docker, one can be set up using
|
Lighthouse requires a MySQL database at this time. For Linux users running docker, one can be set up using
|
||||||
|
@ -31,18 +34,21 @@ 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 modified copy of RPCS3. You can find a working
|
There are also community-provided guides in [the official LBP Union Discord](https://www.lbpunion.com/discord), which
|
||||||
patch [here](https://gist.github.com/jvyden/0d9619f7dd3dbc49f7583486bdacad75).*
|
you can follow at your own discretion.
|
||||||
|
|
||||||
Start by getting a copy of LittleBigPlanet 2 installed. It can be digital (NPUA80662) or disc (BCUS98245). I won't get
|
*Note: This requires a modified copy of RPCS3. You can find a working
|
||||||
into how because if you got this far you should already know what you're doing. For those that don't,
|
version [on our GitHub](https://github.com/LBPUnion/rpcs3).*
|
||||||
the [RPCS3 Quickstart Guide](https://rpcs3.net/quickstart) should cover it.
|
|
||||||
|
Start by getting a copy of LittleBigPlanet 2 installed. It can be digital (NPUA80662) or disc (BCUS98245). For those
|
||||||
|
that don't, the [RPCS3 Quickstart Guide](https://rpcs3.net/quickstart) should cover it.
|
||||||
|
|
||||||
Next, download [UnionPatcher](https://github.com/LBPUnion/UnionPatcher/). Binaries can be found by reading the README.md
|
Next, download [UnionPatcher](https://github.com/LBPUnion/UnionPatcher/). Binaries can be found by reading the README.md
|
||||||
file.
|
file.
|
||||||
|
|
||||||
You should have everything you need now, so open up RPCS3 and go to Utilities -> Decrypt PS3 Binaries. Point this
|
You should have everything you need now, so open up RPCS3 and go to Utilities -> Decrypt PS3 Binaries. Point this
|
||||||
to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`.
|
to `rpcs3/dev_hdd0/game/(title id)/USRDIR/EBOOT.BIN`. You can grab your title id by right clicking the game in RPCS3 and
|
||||||
|
clicking Copy Info -> Copy Serial.
|
||||||
|
|
||||||
This should give you a file named `EBOOT.elf` in the same folder. Next, fire up UnionPatcher (making sure to select the
|
This should give you a file named `EBOOT.elf` in the same folder. Next, fire up UnionPatcher (making sure to select the
|
||||||
correct project to start, e.g. on Mac launch `UnionPatcher.Gui.MacOS`.)
|
correct project to start, e.g. on Mac launch `UnionPatcher.Gui.MacOS`.)
|
||||||
|
@ -68,25 +74,35 @@ Some modifications may require updates to the database schema. You can automatic
|
||||||
|
|
||||||
1. Making sure the tools are installed. You can do this by running `dotnet tool restore`.
|
1. Making sure the tools are installed. You can do this by running `dotnet tool restore`.
|
||||||
2. Making sure `LIGHTHOUSE_DB_CONNECTION_STRING` is set correctly. See the `Running` section for more details.
|
2. Making sure `LIGHTHOUSE_DB_CONNECTION_STRING` is set correctly. See the `Running` section for more details.
|
||||||
3. Making your changes to the database. I won't cover this since if you're making database changes you should know what
|
3. Modifying the database schema via the C# portion of the code. Do not modify the actual SQL database.
|
||||||
you're doing.
|
|
||||||
4. Running `dotnet ef migrations add <NameOfMigrationInPascalCase> --project ProjectLighthouse`.
|
4. Running `dotnet ef migrations add <NameOfMigrationInPascalCase> --project ProjectLighthouse`.
|
||||||
|
|
||||||
|
This process will create a migration file from the changes made in the C# code.
|
||||||
|
|
||||||
|
The new migrations will automatically be applied upon starting Lighthouse.
|
||||||
|
|
||||||
### Running tests
|
### Running tests
|
||||||
|
|
||||||
You can run tests either through your IDE or by running `dotnet tests`.
|
You can run tests either through your IDE or by running `dotnet tests`.
|
||||||
|
|
||||||
Keep in mind while running database tests you need to have `LIGHTHOUSE_DB_CONNECTION_STRING` set.
|
Keep in mind while running database tests you need to have `LIGHTHOUSE_DB_CONNECTION_STRING` set.
|
||||||
|
|
||||||
|
### Continuous Integration (CI) Tips
|
||||||
|
|
||||||
|
- You can skip CI runs for a commit if you specify `[skip ci]` at the beginning of the commit name. This is useful for
|
||||||
|
formatting changes, etc.
|
||||||
|
- When creating your first pull request, CI will not run initially. A team member will have to approve you for use of
|
||||||
|
running CI on a pull request. This is because of GitHub policy.
|
||||||
|
|
||||||
## Compatibility across games and platforms
|
## Compatibility across games and platforms
|
||||||
|
|
||||||
| Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5) |
|
| Game | Console (PS3/Vita/PSP) | Emulator (RPCS3/Vita3k/PPSSPP) | Next-Gen (PS4/PS5/Vita) |
|
||||||
|----------|---------------------------------------|----------------------------------------------------------|------------------------|
|
|----------|---------------------------------------|----------------------------------------------------------|-------------------------|
|
||||||
| LBP1 | Compatible | Incompatible, crashes on entering pod computer | N/A |
|
| LBP1 | Compatible | Incompatible, crashes on entering pod computer | N/A |
|
||||||
| LBP2 | Compatible | Compatible with patched RPCS3 | N/A |
|
| LBP2 | Compatible | Compatible with patched RPCS3 | N/A |
|
||||||
| LBP3 | Somewhat compatible, frequent crashes | Somewhat compatible with patched RPCS3, frequent crashes | Incompatible |
|
| LBP3 | Somewhat compatible, frequent crashes | Somewhat compatible with patched RPCS3, frequent crashes | Incompatible |
|
||||||
| LBP Vita | Compatible | Incompatible, marked as "bootable" on Vita3k | N/A |
|
| LBP Vita | Compatible | Incompatible, marked as "bootable" on Vita3k | N/A |
|
||||||
| LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible |
|
| LBP PSP | Potentially compatible | Incompatible, PSN not supported on PPSSPP | Potentially Compatible |
|
||||||
|
|
||||||
While LBP Vita and LBP PSP can be supported, they are not properly seperated from the mainline games at this time. We
|
While LBP Vita and LBP PSP can be supported, they are not properly seperated from the mainline games at this time. We
|
||||||
recommend you run seperate instances for these games to avoid problems.
|
recommend you run seperate instances for these games to avoid problems.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue