From c704cec58c2ebf539fa13416460856d2946f323e Mon Sep 17 00:00:00 2001
From: turecross321 <51852312+turecross321@users.noreply.github.com>
Date: Wed, 18 Sep 2024 20:47:13 +0200
Subject: [PATCH] Implement API login, logout + tests
---
.../Constants/ExpiryTimes.cs | 1 +
SoundShapesServer.Common/HashHelper.cs | 27 ++
.../SoundShapesServer.Common.csproj | 1 +
.../Verification/SoundShapesSigningKey.cs | 12 +
.../Server/SSTestContext.cs | 23 +-
SoundShapesServer.Tests/Server/TestServer.cs | 12 -
.../AuthenticationTests.cs | 52 +++-
.../Tests/Authentication/TokenTests.cs | 95 ++++++
.../Database/GameDatabaseContext.Ips.cs | 28 ++
.../GameDatabaseContext.RefreshTokens.cs | 54 ++++
.../Database/GameDatabaseContext.Tokens.cs | 28 +-
.../Database/GameDatabaseContext.Users.cs | 35 ++-
.../Database/GameDatabaseContext.cs | 14 +-
.../Api/ApiAuthenticationEndpoints.cs | 96 +++++--
.../Endpoints/Api/ApiUserEndpoints.cs | 18 ++
.../Endpoints/Game/AuthenticationEndpoints.cs | 46 ++-
.../Endpoints/Game/EulaEndpoints.cs | 58 ++--
.../Extensions/TicketExtensions.cs | 30 ++
.../20240917200954_AddGameAuth.Designer.cs | 154 ++++++++++
.../Migrations/20240917200954_AddGameAuth.cs | 51 ++++
.../20240918065653_AddIp.Designer.cs | 221 ++++++++++++++
.../Migrations/20240918065653_AddIp.cs | 83 ++++++
...0918140826_RemoveCountryFromIp.Designer.cs | 216 ++++++++++++++
.../20240918140826_RemoveCountryFromIp.cs | 30 ++
...8164622_AddCreationDateToUsers.Designer.cs | 268 +++++++++++++++++
.../20240918164622_AddCreationDateToUsers.cs | 88 ++++++
...412_AddGenuineNpTicketToTokens.Designer.cs | 271 +++++++++++++++++
...240918170412_AddGenuineNpTicketToTokens.cs | 28 ++
...0918171642_RenameGameAuthNames.Designer.cs | 271 +++++++++++++++++
.../20240918171642_RenameGameAuthNames.cs | 48 ++++
.../20240918183007_Shit.Designer.cs | 271 +++++++++++++++++
.../Migrations/20240918183007_Shit.cs | 22 ++
...14_CascadeDeletionRefreshToken.Designer.cs | 272 ++++++++++++++++++
...40918183414_CascadeDeletionRefreshToken.cs | 41 +++
.../GameDatabaseContextModelSnapshot.cs | 141 ++++++++-
SoundShapesServer/Types/Database/DbIp.cs | 26 ++
.../Types/Database/DbRefreshToken.cs | 23 ++
SoundShapesServer/Types/Database/DbToken.cs | 8 +
SoundShapesServer/Types/Database/DbUser.cs | 6 +
.../Requests/Api/ApiRefreshTokenRequest.cs | 6 +
.../Responses/Api/ApiTypes/ApiResponse.cs | 6 +
.../ApiTypes/Errors/ApiUnauthorizedError.cs | 4 +-
.../Api/DataTypes/ApiLoginResponse.cs | 10 +
.../Api/DataTypes/ApiRefreshTokenResponse.cs | 20 ++
.../Api/DataTypes/ApiTokenResponse.cs | 23 ++
45 files changed, 3151 insertions(+), 87 deletions(-)
create mode 100644 SoundShapesServer.Common/HashHelper.cs
create mode 100644 SoundShapesServer.Common/Verification/SoundShapesSigningKey.cs
rename SoundShapesServer.Tests/Tests/{ => Authentication}/AuthenticationTests.cs (54%)
create mode 100644 SoundShapesServer.Tests/Tests/Authentication/TokenTests.cs
create mode 100644 SoundShapesServer/Database/GameDatabaseContext.Ips.cs
create mode 100644 SoundShapesServer/Database/GameDatabaseContext.RefreshTokens.cs
create mode 100644 SoundShapesServer/Endpoints/Api/ApiUserEndpoints.cs
create mode 100644 SoundShapesServer/Migrations/20240917200954_AddGameAuth.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240917200954_AddGameAuth.cs
create mode 100644 SoundShapesServer/Migrations/20240918065653_AddIp.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918065653_AddIp.cs
create mode 100644 SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.cs
create mode 100644 SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.cs
create mode 100644 SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.cs
create mode 100644 SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.cs
create mode 100644 SoundShapesServer/Migrations/20240918183007_Shit.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918183007_Shit.cs
create mode 100644 SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.Designer.cs
create mode 100644 SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.cs
create mode 100644 SoundShapesServer/Types/Database/DbIp.cs
create mode 100644 SoundShapesServer/Types/Database/DbRefreshToken.cs
create mode 100644 SoundShapesServer/Types/Requests/Api/ApiRefreshTokenRequest.cs
create mode 100644 SoundShapesServer/Types/Responses/Api/DataTypes/ApiLoginResponse.cs
create mode 100644 SoundShapesServer/Types/Responses/Api/DataTypes/ApiRefreshTokenResponse.cs
create mode 100644 SoundShapesServer/Types/Responses/Api/DataTypes/ApiTokenResponse.cs
diff --git a/SoundShapesServer.Common/Constants/ExpiryTimes.cs b/SoundShapesServer.Common/Constants/ExpiryTimes.cs
index 7d91eab..8f33a3b 100644
--- a/SoundShapesServer.Common/Constants/ExpiryTimes.cs
+++ b/SoundShapesServer.Common/Constants/ExpiryTimes.cs
@@ -5,4 +5,5 @@ public class ExpiryTimes
public const int CodeHours = 1;
public const int GameTokenHours = 24;
public const int ApiAccessHours = 4;
+ public const int RefreshTokenHours = 24 * 30;
}
\ No newline at end of file
diff --git a/SoundShapesServer.Common/HashHelper.cs b/SoundShapesServer.Common/HashHelper.cs
new file mode 100644
index 0000000..62e87e6
--- /dev/null
+++ b/SoundShapesServer.Common/HashHelper.cs
@@ -0,0 +1,27 @@
+using System.Security.Cryptography;
+using System.Text;
+
+namespace SoundShapesServer.Common;
+
+public static class HashHelper
+{
+ public static string ComputeSha512Hash(string input)
+ {
+ using SHA512 sha512 = SHA512.Create();
+
+ // Convert the input string to a byte array
+ byte[] inputBytes = Encoding.UTF8.GetBytes(input);
+
+ // Compute the hash
+ byte[] hashBytes = sha512.ComputeHash(inputBytes);
+
+ // Convert the byte array to a hexadecimal string
+ StringBuilder sb = new StringBuilder();
+ foreach (byte b in hashBytes)
+ {
+ sb.Append(b.ToString("x2"));
+ }
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/SoundShapesServer.Common/SoundShapesServer.Common.csproj b/SoundShapesServer.Common/SoundShapesServer.Common.csproj
index 433a85f..d61c544 100644
--- a/SoundShapesServer.Common/SoundShapesServer.Common.csproj
+++ b/SoundShapesServer.Common/SoundShapesServer.Common.csproj
@@ -18,6 +18,7 @@
+
diff --git a/SoundShapesServer.Common/Verification/SoundShapesSigningKey.cs b/SoundShapesServer.Common/Verification/SoundShapesSigningKey.cs
new file mode 100644
index 0000000..b6b77c4
--- /dev/null
+++ b/SoundShapesServer.Common/Verification/SoundShapesSigningKey.cs
@@ -0,0 +1,12 @@
+using NPTicket.Verification.Keys;
+
+namespace SoundShapesServer.Common.Verification;
+
+public class SoundShapesSigningKey : PsnSigningKey
+{
+ public static readonly SoundShapesSigningKey Instance = new();
+ private SoundShapesSigningKey() {}
+
+ public override string CurveX => "39c62d061d4ee35c5f3f7531de0af3cf918346526edac727";
+ public override string CurveY => "a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86";
+}
\ No newline at end of file
diff --git a/SoundShapesServer.Tests/Server/SSTestContext.cs b/SoundShapesServer.Tests/Server/SSTestContext.cs
index 95a235f..1595d2f 100644
--- a/SoundShapesServer.Tests/Server/SSTestContext.cs
+++ b/SoundShapesServer.Tests/Server/SSTestContext.cs
@@ -1,7 +1,5 @@
-using Bunkum.Core.Storage;
-using Bunkum.Protocols.Http.Direct;
+using Bunkum.Protocols.Http.Direct;
using SoundShapesServer.Database;
-using SoundShapesServer.Tests.Database;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;
using Testcontainers.PostgreSql;
@@ -33,21 +31,27 @@ public SSTestContext(Lazy server, GameDatabaseContext database, Ht
private int UserIncrement => this._users++;
- public DbUser CreateUser(string? username = null)
+ public DbUser CreateUser(string? username = null, string email = "user@email.yep", UserRole role = UserRole.Default)
{
username ??= this.UserIncrement.ToString();
- return this.Database.CreateUser(username);
+ DbUser user = this.Database.CreateRegisteredUser(username, email, role);
+ return user;
}
-
+
public HttpClient GetAuthenticatedClient(TokenType type, PlatformType platform = PlatformType.PS3, DbUser? user = null)
{
user ??= this.CreateUser();
- DbToken token = Database.CreateToken(user, type, platform);
-
+ DbToken token = Database.CreateToken(user, type, platform, null, null, null);
+
+ return GetAuthenticatedClient(token);
+ }
+
+ public HttpClient GetAuthenticatedClient(DbToken token)
+ {
HttpClient client = this.Listener.GetClient();
- if (type is TokenType.GameEula or TokenType.GameAccess)
+ if (token.TokenType is TokenType.GameEula or TokenType.GameAccess)
{
client.DefaultRequestHeaders.Add("X-OTG-Identity-SessionId", token.Id.ToString());
}
@@ -58,7 +62,6 @@ public HttpClient GetAuthenticatedClient(TokenType type, PlatformType platform =
return client;
}
-
public void Dispose()
{
this.Database.Dispose();
diff --git a/SoundShapesServer.Tests/Server/TestServer.cs b/SoundShapesServer.Tests/Server/TestServer.cs
index 632cd8c..c99afb1 100644
--- a/SoundShapesServer.Tests/Server/TestServer.cs
+++ b/SoundShapesServer.Tests/Server/TestServer.cs
@@ -1,11 +1,9 @@
using Bunkum.Core.Storage;
-using Bunkum.EntityFrameworkDatabase;
using Bunkum.Protocols.Http;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
using NotEnoughLogs.Sinks;
using SoundShapesServer.Common.Time;
-using SoundShapesServer.Database;
using SoundShapesServer.Tests.Database;
using SoundShapesServer.Types.Config;
@@ -37,14 +35,4 @@ protected override (LoggerConfiguration logConfig, List? sinks) Get
return (logConfig, sinks);
}
-
- protected override void SetupServices()
- {
-
- }
-
- protected override void SetupMiddlewares()
- {
-
- }
}
\ No newline at end of file
diff --git a/SoundShapesServer.Tests/Tests/AuthenticationTests.cs b/SoundShapesServer.Tests/Tests/Authentication/AuthenticationTests.cs
similarity index 54%
rename from SoundShapesServer.Tests/Tests/AuthenticationTests.cs
rename to SoundShapesServer.Tests/Tests/Authentication/AuthenticationTests.cs
index 2c7044a..6701f26 100644
--- a/SoundShapesServer.Tests/Tests/AuthenticationTests.cs
+++ b/SoundShapesServer.Tests/Tests/Authentication/AuthenticationTests.cs
@@ -1,11 +1,14 @@
using System.Net.Http.Json;
+using SoundShapesServer.Common;
+using SoundShapesServer.Endpoints.Api;
using SoundShapesServer.Tests.Server;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;
using SoundShapesServer.Types.Requests.Api;
-using TestContext = NUnit.Framework.TestContext;
+using SoundShapesServer.Types.Responses.Api.ApiTypes;
+using SoundShapesServer.Types.Responses.Api.DataTypes;
-namespace SoundShapesServer.Tests.Tests;
+namespace SoundShapesServer.Tests.Tests.Authentication;
public class AuthenticationTests : ServerTest
{
@@ -14,7 +17,7 @@ public void AuthenticationEnforcementWorks()
{
using SSTestContext context = this.GetServer();
- string apiEndpoint = "/api/v1/me";
+ string apiEndpoint = "/api/v1/users/me";
string gameEulaEndpoint = "/otg/ps3/SCEA/en/~eula.get";
//string gameEndpoint = ""; // Todo: add any authenticated endpoint to this when there is one implemented
@@ -35,6 +38,49 @@ public void AuthenticationEnforcementWorks()
}
}
+ [Test]
+ public void ApiLoginAndRefreshTokenWork()
+ {
+ using SSTestContext context = this.GetServer();
+
+ string email = "bigBoss@mail.com";
+ string password = "password";
+
+ string passwordSha512 = HashHelper.ComputeSha512Hash(password);
+ string passwordBcrypt = BCrypt.Net.BCrypt.HashPassword(passwordSha512, ApiAuthenticationEndpoints.WorkFactor);
+ DbUser user = context.CreateUser(email: email);
+ user = context.Database.SetUserPassword(user, passwordBcrypt);
+
+ using HttpClient client = context.Http;
+
+ HttpResponseMessage response = client.GetAsync("/api/v1/users/me").Result;
+ Assert.That(!response.IsSuccessStatusCode);
+
+ response = client.PostAsJsonAsync("/api/v1/logIn", new ApiLogInRequest
+ {
+ Email = email,
+ PasswordSha512 = passwordSha512
+ }).Result;
+
+ Assert.That(response.IsSuccessStatusCode);
+
+ ApiLoginResponse? deSerialized =
+ response.Content.ReadFromJsonAsync>().Result!.Data;
+ Assert.That(deSerialized != null);
+
+ Assert.That(context.Database.GetTokenWithId(deSerialized!.AccessToken.Id) != null);
+
+ response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
+ {
+ RefreshTokenId = deSerialized!.RefreshToken.Id
+ }).Result;
+
+ deSerialized = response.Content.ReadFromJsonAsync>().Result!.Data;
+ Assert.That(deSerialized != null);
+
+ Assert.That(context.Database.GetTokenWithId(deSerialized!.AccessToken.Id) != null);
+ }
+
// todo: fix the email service so that this test works
/*
[Test]
diff --git a/SoundShapesServer.Tests/Tests/Authentication/TokenTests.cs b/SoundShapesServer.Tests/Tests/Authentication/TokenTests.cs
new file mode 100644
index 0000000..5f71291
--- /dev/null
+++ b/SoundShapesServer.Tests/Tests/Authentication/TokenTests.cs
@@ -0,0 +1,95 @@
+using System.Net.Http.Json;
+using SoundShapesServer.Common.Constants;
+using SoundShapesServer.Tests.Server;
+using SoundShapesServer.Types;
+using SoundShapesServer.Types.Database;
+using SoundShapesServer.Types.Requests.Api;
+using SoundShapesServer.Types.Responses.Api.ApiTypes;
+using SoundShapesServer.Types.Responses.Api.DataTypes;
+
+namespace SoundShapesServer.Tests.Tests.Authentication;
+
+public class TokenTests : ServerTest
+{
+ [Test]
+ public void RevokeTokenWorks()
+ {
+ using SSTestContext context = this.GetServer();
+
+ DbUser user = context.CreateUser();
+ DbRefreshToken refreshToken = context.Database.CreateRefreshToken(user);
+
+ List tokens = new();
+ for (int i = 0; i < 5; i++)
+ {
+ tokens.Add(context.Database.CreateApiTokenWithRefreshToken(refreshToken));
+ }
+
+ HttpClient client = context.GetAuthenticatedClient(tokens.First());
+
+ HttpResponseMessage response = client.PostAsync("/api/v1/revokeToken", null).Result;
+ Assert.That(response.IsSuccessStatusCode);
+ foreach (DbToken token in tokens)
+ {
+ Assert.That(context.Database.GetTokenWithId(token.Id), Is.EqualTo(null));
+ }
+ Assert.That(context.Database.GetRefreshTokenWithId(refreshToken.Id), Is.EqualTo(null));
+ }
+
+ [Test]
+ public void TokenExpiryWorks()
+ {
+ using SSTestContext context = this.GetServer();
+
+ HttpClient client = context.GetAuthenticatedClient(TokenType.ApiAccess);
+
+ HttpResponseMessage response = client.GetAsync("/api/v1/users/me").Result;
+ Assert.That(response.IsSuccessStatusCode);
+
+ context.Time.Now = context.Time.Now.AddYears(1);
+
+ response = client.GetAsync("/api/v1/users/me").Result;
+ Assert.That(!response.IsSuccessStatusCode);
+ }
+
+ [Test]
+ public void RefreshTokenExpiryWorks()
+ {
+ using SSTestContext context = this.GetServer();
+ using HttpClient client = context.Http;
+
+ DbUser user = context.CreateUser();
+ DbRefreshToken refreshToken = context.Database.CreateRefreshToken(user);
+
+ // get right before refresh token expires
+ context.Time.Now = context.Time.Now.AddHours(ExpiryTimes.RefreshTokenHours).AddMinutes(-1);
+ HttpResponseMessage response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
+ {
+ RefreshTokenId = refreshToken.Id
+ }).Result;
+ Assert.That(response.IsSuccessStatusCode);
+
+ // assure that the generated access token works
+ ApiLoginResponse? deSerialized =
+ response.Content.ReadFromJsonAsync>().Result!.Data;
+ Assert.That(deSerialized != null);
+ DbToken? accessToken = context.Database.GetTokenWithId(deSerialized!.AccessToken.Id);
+ Assert.That(accessToken != null);
+
+ // get right after refresh token expires
+ context.Time.Now = context.Time.Now.AddMinutes(2);
+
+ response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
+ {
+ RefreshTokenId = refreshToken.Id
+ }).Result;
+
+ // check that the refresh token no longer works after the expiry date
+ Assert.That(!response.IsSuccessStatusCode);
+
+ // check that the access token was generated with the refresh token has been revoked
+ using HttpClient authClient = context.GetAuthenticatedClient(accessToken!);
+ response = client.GetAsync("/api/v1/users/me").Result;
+ Assert.That(!response.IsSuccessStatusCode);
+ }
+}
\ No newline at end of file
diff --git a/SoundShapesServer/Database/GameDatabaseContext.Ips.cs b/SoundShapesServer/Database/GameDatabaseContext.Ips.cs
new file mode 100644
index 0000000..4a4d7fd
--- /dev/null
+++ b/SoundShapesServer/Database/GameDatabaseContext.Ips.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using SoundShapesServer.Types.Database;
+
+namespace SoundShapesServer.Database;
+
+public partial class GameDatabaseContext
+{
+ public DbIp GetOrCreateIp(DbUser user, string ipAddress)
+ {
+ DbIp? existingIp = Ips.Include(i => i.User)
+ .FirstOrDefault(i => i.UserId == user.Id && i.IpAddress == ipAddress);
+
+ if (existingIp != null)
+ return existingIp;
+
+ EntityEntry ip = Ips.Add(new DbIp
+ {
+ IpAddress = ipAddress,
+ CreationDate = Time.Now,
+ UserId = user.Id
+ });
+
+ SaveChanges();
+
+ return ip.Entity;
+ }
+}
\ No newline at end of file
diff --git a/SoundShapesServer/Database/GameDatabaseContext.RefreshTokens.cs b/SoundShapesServer/Database/GameDatabaseContext.RefreshTokens.cs
new file mode 100644
index 0000000..ed11635
--- /dev/null
+++ b/SoundShapesServer/Database/GameDatabaseContext.RefreshTokens.cs
@@ -0,0 +1,54 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using SoundShapesServer.Common.Constants;
+using SoundShapesServer.Types;
+using SoundShapesServer.Types.Database;
+
+namespace SoundShapesServer.Database;
+
+public partial class GameDatabaseContext
+{
+ public DbToken CreateApiTokenWithRefreshToken(DbRefreshToken refresh)
+ {
+ return CreateToken(refresh.User, TokenType.ApiAccess, null, null, refresh, null);
+ }
+
+ public DbRefreshToken? GetRefreshTokenWithId(Guid guid)
+ {
+ DbRefreshToken? token = RefreshTokens
+ .Include(t => t.User)
+ .FirstOrDefault(t => t.Id == guid);
+
+ if (_time.Now >= token?.ExpiryDate)
+ {
+ RemoveRefreshToken(token);
+ return null;
+ }
+
+ return token;
+ }
+
+ private void RemoveRefreshToken(DbRefreshToken token)
+ {
+ RefreshTokens.Remove(token);
+
+ SaveChanges();
+ }
+
+ public DbRefreshToken CreateRefreshToken(DbUser user)
+ {
+ EntityEntry token = RefreshTokens.Add(new DbRefreshToken
+ {
+ UserId = user.Id,
+ CreationDate = _time.Now,
+ ExpiryDate = _time.Now.AddHours(ExpiryTimes.RefreshTokenHours)
+ });
+
+ SaveChanges();
+
+ // Reload to load the ID
+ token.Reload();
+
+ return token.Entity;
+ }
+}
\ No newline at end of file
diff --git a/SoundShapesServer/Database/GameDatabaseContext.Tokens.cs b/SoundShapesServer/Database/GameDatabaseContext.Tokens.cs
index a0e848f..9edd681 100644
--- a/SoundShapesServer/Database/GameDatabaseContext.Tokens.cs
+++ b/SoundShapesServer/Database/GameDatabaseContext.Tokens.cs
@@ -10,10 +10,14 @@ public partial class GameDatabaseContext
{
public DbToken? GetTokenWithId(Guid guid)
{
- return Tokens.Include(t => t.User).FirstOrDefault(t => t.Id == guid);
+ return Tokens
+ .Include(t => t.User)
+ .Include(t => t.RefreshToken)
+ .FirstOrDefault(t => t.Id == guid);
}
- public DbToken CreateToken(DbUser user, TokenType tokenType, PlatformType? platformType)
+ public DbToken CreateToken(DbUser user, TokenType tokenType, PlatformType? platformType, DbIp? ip,
+ DbRefreshToken? refreshToken, bool? genuineNpTicket)
{
int expiryHours = tokenType switch
{
@@ -23,13 +27,24 @@ public DbToken CreateToken(DbUser user, TokenType tokenType, PlatformType? platf
_ => throw new ArgumentOutOfRangeException(nameof(tokenType), tokenType, null)
};
+ DateTimeOffset expiry = _time.Now.AddHours(expiryHours);
+
+ // If there is a refresh token and it expires before this would normally expire, use the refresh expiry date instead.
+ // This is to prevent a situation where the refresh token is expired but there are still tokens
+ // generated with it that are usable.
+ if (refreshToken != null && refreshToken.ExpiryDate < expiry)
+ expiry = refreshToken.ExpiryDate;
+
EntityEntry token = Tokens.Add(new DbToken
{
UserId = user.Id,
TokenType = tokenType,
CreationDate = _time.Now,
- ExpiryDate = _time.Now.AddHours(expiryHours),
+ ExpiryDate = expiry,
Platform = platformType,
+ IpId = ip?.Id,
+ RefreshTokenId = refreshToken?.Id,
+ GenuineNpTicket = genuineNpTicket,
});
SaveChanges();
@@ -42,7 +57,14 @@ public DbToken CreateToken(DbUser user, TokenType tokenType, PlatformType? platf
public void RemoveToken(DbToken token)
{
+ if (token.RefreshToken != null)
+ {
+ RemoveRefreshToken(token.RefreshToken);
+ return;
+ }
+
Tokens.Remove(token);
SaveChanges();
+
}
}
\ No newline at end of file
diff --git a/SoundShapesServer/Database/GameDatabaseContext.Users.cs b/SoundShapesServer/Database/GameDatabaseContext.Users.cs
index a105095..8fbc54d 100644
--- a/SoundShapesServer/Database/GameDatabaseContext.Users.cs
+++ b/SoundShapesServer/Database/GameDatabaseContext.Users.cs
@@ -23,7 +23,28 @@ public DbUser CreateUser(string name)
Name = name,
Role = UserRole.Default,
FinishedRegistration = false,
- VerifiedEmail = false
+ VerifiedEmail = false,
+ CreationDate = Time.Now
+ });
+
+ SaveChanges();
+
+ // Reload to load the ID
+ user.Reload();
+
+ return user.Entity;
+ }
+
+ public DbUser CreateRegisteredUser(string name, string email, UserRole role)
+ {
+ EntityEntry user = Users.Add(new DbUser
+ {
+ Name = name,
+ Role = role,
+ FinishedRegistration = true,
+ VerifiedEmail = true,
+ CreationDate = Time.Now,
+ EmailAddress = email
});
SaveChanges();
@@ -34,28 +55,32 @@ public DbUser CreateUser(string name)
return user.Entity;
}
- public void SetUserEmail(DbUser user, string email)
+ public DbUser SetUserEmail(DbUser user, string email)
{
user.EmailAddress = email;
user.VerifiedEmail = false;
SaveChanges();
+ return user;
}
- public void VerifyEmail(DbUser user)
+ public DbUser VerifyEmail(DbUser user)
{
user.VerifiedEmail = true;
SaveChanges();
+ return user;
}
- public void SetUserPassword(DbUser user, string passwordBcrypt)
+ public DbUser SetUserPassword(DbUser user, string passwordBcrypt)
{
user.PasswordBcrypt = passwordBcrypt;
SaveChanges();
+ return user;
}
- public void FinishUserRegistration(DbUser user)
+ public DbUser FinishUserRegistration(DbUser user)
{
user.FinishedRegistration = true;
SaveChanges();
+ return user;
}
}
\ No newline at end of file
diff --git a/SoundShapesServer/Database/GameDatabaseContext.cs b/SoundShapesServer/Database/GameDatabaseContext.cs
index 4e4387d..a836bf8 100644
--- a/SoundShapesServer/Database/GameDatabaseContext.cs
+++ b/SoundShapesServer/Database/GameDatabaseContext.cs
@@ -24,10 +24,22 @@ public GameDatabaseContext(string connection, IDateTimeProvider timeProvider)
private readonly string? _connectionString;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(this._connectionString);
-
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasMany(e => e.Tokens)
+ .WithOne(e => e.RefreshToken)
+ .HasForeignKey(e => e.RefreshTokenId)
+ .HasPrincipalKey(e => e.Id)
+ .OnDelete(DeleteBehavior.Cascade);
+ }
+
private DbSet Users { get; set; }
private DbSet Codes { get; set; }
private DbSet Tokens { get; set; }
+ private DbSet RefreshTokens { get; set; }
+ private DbSet Ips { get; set; }
public override void Dispose()
{
diff --git a/SoundShapesServer/Endpoints/Api/ApiAuthenticationEndpoints.cs b/SoundShapesServer/Endpoints/Api/ApiAuthenticationEndpoints.cs
index 4242549..d7e2b6f 100644
--- a/SoundShapesServer/Endpoints/Api/ApiAuthenticationEndpoints.cs
+++ b/SoundShapesServer/Endpoints/Api/ApiAuthenticationEndpoints.cs
@@ -13,6 +13,7 @@
using SoundShapesServer.Types.Responses.Api.ApiTypes;
using SoundShapesServer.Types.Responses.Api.ApiTypes.Errors;
using SoundShapesServer.Types.Responses.Api.DataTypes;
+using static BCrypt.Net.BCrypt;
namespace SoundShapesServer.Endpoints.Api;
@@ -22,13 +23,13 @@ public class ApiAuthenticationEndpoints : EndpointGroup
/// If increased, passwords will automatically be rehashed at login time to use the new WorkFactor
/// If decreased, passwords will stay at higher WorkFactor until reset
///
- private const int WorkFactor = 14;
+ public const int WorkFactor = 14;
///
/// A randomly generated password.
/// Used to prevent against timing attacks.
///
- private static readonly string FakePassword = BCrypt.Net.BCrypt.HashPassword(Random.Shared.Next().ToString(), WorkFactor);
+ private static readonly string FakePassword = HashPassword(Random.Shared.Next().ToString(), WorkFactor);
[DocError(typeof(ApiUnauthorizedError), ApiUnauthorizedError.InvalidCodeWhen)]
[DocError(typeof(ApiBadRequestError), ApiBadRequestError.InvalidEmailWhen)]
@@ -88,11 +89,14 @@ public ApiOkResponse VerifyEmail(RequestContext context, GameDatabaseContext dat
if (code == null)
return ApiUnauthorizedError.InvalidCode;
- context.Logger.LogInfo(BunkumCategory.Authentication, $"{code.User} successfully verified their email.");
+ DbUser user = database.VerifyEmail(code.User);
+ if (!user.FinishedRegistration)
+ {
+ user = database.FinishUserRegistration(code.User);
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully finish their registration.");
+ }
- database.VerifyEmail(code.User);
- if (!code.User.FinishedRegistration)
- database.FinishUserRegistration(code.User);
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully verified their email.");
return new ApiOkResponse();
}
@@ -147,8 +151,8 @@ Please use the code below to reset your password.
[DocSummary("Set a new password.")]
[RateLimitSettings(300, 4, 300, "setPassword")]
[Authentication(false)]
- [ApiEndpoint("setPassword", HttpMethods.Post)]
- public ApiOkResponse SetPassword(RequestContext context, GameDatabaseContext database,
+ [ApiEndpoint("resetPassword", HttpMethods.Post)]
+ public ApiOkResponse ResetPassword(RequestContext context, GameDatabaseContext database,
ApiSetPasswordRequest body)
{
DbCode? codeToken = database.GetCode(body.Code, CodeType.SetPassword);
@@ -158,7 +162,7 @@ public ApiOkResponse SetPassword(RequestContext context, GameDatabaseContext dat
if (body.PasswordSha512.Length != 128 || !CommonPatterns.Sha512Regex().IsMatch(body.PasswordSha512))
return ApiBadRequestError.PasswordIsNotHashed;
- string? passwordBcrypt = BCrypt.Net.BCrypt.HashPassword(body.PasswordSha512, WorkFactor);
+ string? passwordBcrypt = HashPassword(body.PasswordSha512, WorkFactor);
if (passwordBcrypt == null) return ApiInternalServerError.CouldNotBcryptPassword;
database.SetUserPassword(codeToken.User, passwordBcrypt);
@@ -204,7 +208,7 @@ public ApiOkResponse Register(RequestContext context, GameDatabaseContext databa
if (!CommonPatterns.EmailAddressRegex().IsMatch(body.Email))
return ApiBadRequestError.InvalidEmail;
- string? passwordBcrypt = BCrypt.Net.BCrypt.HashPassword(body.PasswordSha512, WorkFactor);
+ string? passwordBcrypt = HashPassword(body.PasswordSha512, WorkFactor);
if (passwordBcrypt == null)
return ApiInternalServerError.CouldNotBcryptPassword;
@@ -240,12 +244,72 @@ Please use the verification code below to verify your email address and finish t
return new ApiOkResponse();
}
+
+ [DocResponseBody(typeof(ApiLoginResponse))]
+ [DocRequestBody(typeof(ApiLogInRequest))]
+ [DocError(typeof(ApiUnauthorizedError), ApiUnauthorizedError.InvalidEmailOrPasswordWhen)]
+ [RateLimitSettings(300, 10, 300, "auth")]
+ [Authentication(false)]
+ [ApiEndpoint("logIn", HttpMethods.Post)]
+ public ApiResponse LogIn(RequestContext context, GameDatabaseContext database, ApiLogInRequest body)
+ {
+ DbUser? user = database.GetUserWithEmail(body.Email);
+ if (user == null)
+ {
+ // Do the work of checking the password if there was no user found to avoid timing attacks.
+ _ = Verify(body.PasswordSha512, FakePassword);
+
+ return ApiUnauthorizedError.InvalidEmailOrPassword;
+ }
+
+ if (!Verify(body.PasswordSha512, user.PasswordBcrypt))
+ return ApiUnauthorizedError.InvalidEmailOrPassword;
+
+ if (PasswordNeedsRehash(user.PasswordBcrypt, WorkFactor))
+ database.SetUserPassword(user, HashPassword(body.PasswordSha512, WorkFactor));
+
+ DbRefreshToken refreshToken = database.CreateRefreshToken(user);
+ DbToken token = database.CreateToken(user, TokenType.ApiAccess, null, null, refreshToken, null);
+
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully logged in through the API");
+
+ return new ApiLoginResponse
+ {
+ User = ApiFullUserResponse.FromDb(user),
+ AccessToken = ApiTokenResponse.FromDb(token),
+ RefreshToken = ApiRefreshTokenResponse.FromDb(refreshToken)
+ };
+ }
- [DocSummary("Retrieves the logged in user")]
- [DocResponseBody(typeof(ApiFullUserResponse))]
- [ApiEndpoint("me")]
- public ApiFullUserResponse GetSelf(RequestContext context, DbUser user)
+ [DocSummary("Log in with a refresh token.")]
+ [DocResponseBody(typeof(ApiLoginResponse))]
+ [DocRequestBody(typeof(ApiRefreshTokenRequest))]
+ [DocError(typeof(ApiNotFoundError), ApiNotFoundError.RefreshTokenDoesNotExistWhen)]
+ [RateLimitSettings(300, 10, 300, "auth")]
+ [Authentication(false)]
+ [ApiEndpoint("refreshToken", HttpMethods.Post)]
+ public ApiResponse LogInWithRefreshToken(RequestContext context, GameDatabaseContext database,
+ ApiRefreshTokenRequest body)
{
- return ApiFullUserResponse.FromDb(user);
- }
+ DbRefreshToken? refreshToken = database.GetRefreshTokenWithId(body.RefreshTokenId);
+ if (refreshToken == null)
+ return ApiNotFoundError.RefreshTokenDoesNotExist;
+
+ DbToken token = database.CreateApiTokenWithRefreshToken(refreshToken);
+
+ return new ApiLoginResponse
+ {
+ User = ApiFullUserResponse.FromDb(refreshToken.User),
+ AccessToken = ApiTokenResponse.FromDb(token),
+ RefreshToken = ApiRefreshTokenResponse.FromDb(refreshToken)
+ };
+ }
+
+ [DocSummary("Revoke your access token and its associated refresh token with all its other tokens.")]
+ [ApiEndpoint("revokeToken", HttpMethods.Post)]
+ public ApiOkResponse LogOut(RequestContext context, GameDatabaseContext database, DbToken token)
+ {
+ database.RemoveToken(token);
+ return new ApiOkResponse();
+ }
}
\ No newline at end of file
diff --git a/SoundShapesServer/Endpoints/Api/ApiUserEndpoints.cs b/SoundShapesServer/Endpoints/Api/ApiUserEndpoints.cs
new file mode 100644
index 0000000..56fa6f9
--- /dev/null
+++ b/SoundShapesServer/Endpoints/Api/ApiUserEndpoints.cs
@@ -0,0 +1,18 @@
+using AttribDoc.Attributes;
+using Bunkum.Core;
+using Bunkum.Core.Endpoints;
+using SoundShapesServer.Types.Database;
+using SoundShapesServer.Types.Responses.Api.DataTypes;
+
+namespace SoundShapesServer.Endpoints.Api;
+
+public class ApiUserEndpoints : EndpointGroup
+{
+ [DocSummary("Retrieves the logged in user")]
+ [DocResponseBody(typeof(ApiFullUserResponse))]
+ [ApiEndpoint("users/me")]
+ public ApiFullUserResponse GetSelf(RequestContext context, DbUser user)
+ {
+ return ApiFullUserResponse.FromDb(user);
+ }
+}
\ No newline at end of file
diff --git a/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs b/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs
index 2e6efa6..6e2ab04 100644
--- a/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs
+++ b/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs
@@ -1,4 +1,5 @@
-using Bunkum.Core;
+using System.Net;
+using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Responses;
using Bunkum.Listener.Protocol;
@@ -42,21 +43,56 @@ public Response Authenticate(RequestContext context, GameDatabaseContext databas
if (platform == null)
{
context.Logger.LogWarning(BunkumCategory.Authentication, $"Unable to determine PlatformType ({ticket.Username}).");
+ // todo: notification about unable to determine platform type
return BadRequest;
}
DbUser user = database.GetUserWithName(ticket.Username) ?? database.CreateUser(ticket.Username);
DbToken token;
- if (!user.FinishedRegistration)
+ // todo: check for bans
+
+ bool allowAuthentication = false;
+ DbIp? ip = null; // required for IP authentication
+
+ bool genuineTicket = ticket.IsGenuine((MemoryStream)body, database.Time.Now, platform);
+
+ if (genuineTicket)
{
- context.Logger.LogInfo(BunkumCategory.Authentication, $"Creating eula token ({platform}) for {user}.");
- token = database.CreateToken(user, TokenType.GameEula, platform);
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} has a genuine ticket.");
+ if (platform is PlatformType.PS3 or PlatformType.PS4 or PlatformType.PSVita
+ && user.PsnAuthorization)
+ allowAuthentication = true;
+ if (user.RpcnAuthorization && platform is PlatformType.RPCS3)
+ allowAuthentication = true;
}
else
+ {
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} doesn't have a genuine ticket.");
+ }
+
+ if (user.IpAuthorization)
+ {
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"Tracking IP address for {user} as " +
+ $"they have enabled IP authentication.");
+ ip = database.GetOrCreateIp(user, ((IPEndPoint)context.RemoteEndpoint).Address.ToString());
+
+ if (ip.Authorized)
+ {
+ allowAuthentication = true;
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} has an authorized IP address.");
+ }
+ }
+
+ if (allowAuthentication)
{
context.Logger.LogInfo(BunkumCategory.Authentication, $"Creating access token ({platform}) for {user}.");
- token = database.CreateToken(user, TokenType.GameAccess, platform);
+ token = database.CreateToken(user, TokenType.GameAccess, platform, ip, null, genuineTicket);
+ }
+ else
+ {
+ context.Logger.LogInfo(BunkumCategory.Authentication, $"Creating eula token ({platform}) for {user}.");
+ token = database.CreateToken(user, TokenType.GameEula, platform, ip, null, genuineTicket);
}
context.ResponseHeaders.Add("set-cookie", $"OTG-Identity-SessionId={token.Id};Version=1;Path=/");
diff --git a/SoundShapesServer/Endpoints/Game/EulaEndpoints.cs b/SoundShapesServer/Endpoints/Game/EulaEndpoints.cs
index 3c429d0..af2f375 100644
--- a/SoundShapesServer/Endpoints/Game/EulaEndpoints.cs
+++ b/SoundShapesServer/Endpoints/Game/EulaEndpoints.cs
@@ -19,39 +19,49 @@ public string GetEula(RequestContext context, GameDatabaseContext database, Bunk
string eula = "";
// this is included when we want to make sure that the eula is always shown (e.g. when showing the registration code)
- bool includeDate;
-
- switch (token.TokenType)
+ bool includeDate = false;
+
+ if (token.TokenType == TokenType.GameAccess)
{
- case TokenType.GameAccess:
- includeDate = false;
- eula = $"Welcome {user.Name}!";
- break;
- case TokenType.GameEula:
- includeDate = true;
- if (!user.FinishedRegistration)
- {
- context.Logger.LogInfo(BunkumCategory.Authentication, "Creating initialize registration code for new user: " + user.Name);
- DbCode code = database.CreateCode(user, CodeType.Registration);
+ includeDate = false;
+ eula = $"Welcome {user.Name}!";
+ }
+ else if (!user.FinishedRegistration)
+ {
+ context.Logger.LogInfo(BunkumCategory.Authentication, "Creating initialize registration code for new user: " + user.Name);
+ DbCode code = database.CreateCode(user, CodeType.Registration);
- eula =
- $"You currently do not have an account. To proceed, go to {bunkumConfig.ExternalUrl}/register and follow the instructions.\n" +
- $"Your registration code is \"{code.Code}\".";
- }
-
-
- // todo: inform about bans, or if token auth / ip auth has been enabled etc.
- break;
- default:
- throw new ArgumentOutOfRangeException();
+ eula =
+ $"You currently do not have an account. To proceed, go to {config.WebsiteUrl}/register and follow the instructions.\n" +
+ $"Your registration code is \"{code.Code}\".";
}
+ else
+ {
+ eula = "Your session has not been authorized.\n\n" +
+ $"To proceed, go to {config.WebsiteUrl}/authorization " +
+ "and perform one of the following actions:\n\n";
+ if (token.GenuineNpTicket == true)
+ {
+ switch (token.Platform)
+ {
+ case PlatformType.RPCS3:
+ eula += "- Enable RPCN Authorization\n";
+ break;
+ case PlatformType.PS3 or PlatformType.PS4 or PlatformType.PSVita:
+ eula += "- Enable PSN Authorization\n";
+ break;
+ }
+ }
+
+ eula += "- Enable IP Authorization";
+ }
eula = eula + "\n \n" + config.EulaText + "\n \n" + Licenses.AGPLNotice;
if (includeDate)
eula += "\n \n" + DateTimeOffset.UtcNow;
- return eula; // todo: investigate vita shit
+ return eula;
}
diff --git a/SoundShapesServer/Extensions/TicketExtensions.cs b/SoundShapesServer/Extensions/TicketExtensions.cs
index 7780043..ab0b28f 100644
--- a/SoundShapesServer/Extensions/TicketExtensions.cs
+++ b/SoundShapesServer/Extensions/TicketExtensions.cs
@@ -1,10 +1,40 @@
using NPTicket;
+using NPTicket.Verification;
+using NPTicket.Verification.Keys;
+using SoundShapesServer.Common.Verification;
using SoundShapesServer.Types;
namespace SoundShapesServer.Extensions;
public static class TicketExtensions
{
+ public static bool IsGenuine(this Ticket ticket, MemoryStream body, DateTimeOffset now, PlatformType? platformType = null)
+ {
+ ITicketSigningKey signingKey;
+
+ platformType ??= ticket.GetPlatformType();
+ switch (platformType)
+ {
+ case PlatformType.RPCS3:
+ signingKey = RpcnSigningKey.Instance;
+ break;
+ case PlatformType.PSVita:
+ case PlatformType.PS3:
+ case PlatformType.PS4:
+ signingKey = SoundShapesSigningKey.Instance;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(platformType));
+ }
+
+ // Dont allow use of expired tickets
+ if (now > ticket.ExpiryDate)
+ return false;
+
+ TicketVerifier verifier = new(body.ToArray(), ticket, signingKey);
+ return verifier.IsTicketValid();
+ }
+
public static PlatformType? GetPlatformType(this Ticket ticket)
{
if (ticket.SignatureIdentifier == "RPCN" || ticket.IssuerId == 0x33333333)
diff --git a/SoundShapesServer/Migrations/20240917200954_AddGameAuth.Designer.cs b/SoundShapesServer/Migrations/20240917200954_AddGameAuth.Designer.cs
new file mode 100644
index 0000000..e2185a4
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240917200954_AddGameAuth.Designer.cs
@@ -0,0 +1,154 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240917200954_AddGameAuth")]
+ partial class AddGameAuth
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowIpAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPsnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRpcnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240917200954_AddGameAuth.cs b/SoundShapesServer/Migrations/20240917200954_AddGameAuth.cs
new file mode 100644
index 0000000..49eac68
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240917200954_AddGameAuth.cs
@@ -0,0 +1,51 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class AddGameAuth : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AllowIpAuthentication",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "AllowPsnAuthentication",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "AllowRpcnAuthentication",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "AllowIpAuthentication",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "AllowPsnAuthentication",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "AllowRpcnAuthentication",
+ table: "Users");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918065653_AddIp.Designer.cs b/SoundShapesServer/Migrations/20240918065653_AddIp.Designer.cs
new file mode 100644
index 0000000..5d1e57c
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918065653_AddIp.Designer.cs
@@ -0,0 +1,221 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918065653_AddIp")]
+ partial class AddIp
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Country")
+ .IsRequired()
+ .HasMaxLength(2)
+ .HasColumnType("character varying(2)");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasMaxLength(39)
+ .HasColumnType("character varying(39)");
+
+ b.Property("OneTimeUse")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Ips");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpId")
+ .HasColumnType("integer");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IpId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowIpAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPsnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRpcnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbIp", "Ip")
+ .WithMany("Tokens")
+ .HasForeignKey("IpId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Ip");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Navigation("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918065653_AddIp.cs b/SoundShapesServer/Migrations/20240918065653_AddIp.cs
new file mode 100644
index 0000000..246529a
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918065653_AddIp.cs
@@ -0,0 +1,83 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class AddIp : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "IpId",
+ table: "Tokens",
+ type: "integer",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "Ips",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ UserId = table.Column(type: "uuid", nullable: false),
+ IpAddress = table.Column(type: "character varying(39)", maxLength: 39, nullable: false),
+ Country = table.Column(type: "character varying(2)", maxLength: 2, nullable: false),
+ CreationDate = table.Column(type: "timestamp with time zone", nullable: false),
+ AuthorizedDate = table.Column(type: "timestamp with time zone", nullable: false),
+ Authorized = table.Column(type: "boolean", nullable: false),
+ OneTimeUse = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Ips", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Ips_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Tokens_IpId",
+ table: "Tokens",
+ column: "IpId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Ips_UserId",
+ table: "Ips",
+ column: "UserId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Tokens_Ips_IpId",
+ table: "Tokens",
+ column: "IpId",
+ principalTable: "Ips",
+ principalColumn: "Id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Tokens_Ips_IpId",
+ table: "Tokens");
+
+ migrationBuilder.DropTable(
+ name: "Ips");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Tokens_IpId",
+ table: "Tokens");
+
+ migrationBuilder.DropColumn(
+ name: "IpId",
+ table: "Tokens");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.Designer.cs b/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.Designer.cs
new file mode 100644
index 0000000..516d99c
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.Designer.cs
@@ -0,0 +1,216 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918140826_RemoveCountryFromIp")]
+ partial class RemoveCountryFromIp
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasMaxLength(39)
+ .HasColumnType("character varying(39)");
+
+ b.Property("OneTimeUse")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Ips");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpId")
+ .HasColumnType("integer");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IpId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowIpAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPsnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRpcnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbIp", "Ip")
+ .WithMany("Tokens")
+ .HasForeignKey("IpId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Ip");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Navigation("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.cs b/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.cs
new file mode 100644
index 0000000..c393786
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918140826_RemoveCountryFromIp.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class RemoveCountryFromIp : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Country",
+ table: "Ips");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Country",
+ table: "Ips",
+ type: "character varying(2)",
+ maxLength: 2,
+ nullable: false,
+ defaultValue: "");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.Designer.cs b/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.Designer.cs
new file mode 100644
index 0000000..5a56ad7
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.Designer.cs
@@ -0,0 +1,268 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918164622_AddCreationDateToUsers")]
+ partial class AddCreationDateToUsers
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasMaxLength(39)
+ .HasColumnType("character varying(39)");
+
+ b.Property("OneTimeUse")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Ips");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpId")
+ .HasColumnType("integer");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("RefreshTokenId")
+ .HasColumnType("uuid");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IpId");
+
+ b.HasIndex("RefreshTokenId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowIpAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPsnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRpcnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbIp", "Ip")
+ .WithMany("Tokens")
+ .HasForeignKey("IpId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbRefreshToken", "RefreshToken")
+ .WithMany("Tokens")
+ .HasForeignKey("RefreshTokenId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Ip");
+
+ b.Navigation("RefreshToken");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Navigation("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Navigation("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.cs b/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.cs
new file mode 100644
index 0000000..efe957c
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918164622_AddCreationDateToUsers.cs
@@ -0,0 +1,88 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class AddCreationDateToUsers : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "CreationDate",
+ table: "Users",
+ type: "timestamp with time zone",
+ nullable: false,
+ defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
+
+ migrationBuilder.AddColumn(
+ name: "RefreshTokenId",
+ table: "Tokens",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "RefreshTokens",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ UserId = table.Column(type: "uuid", nullable: false),
+ CreationDate = table.Column(type: "timestamp with time zone", nullable: false),
+ ExpiryDate = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_RefreshTokens", x => x.Id);
+ table.ForeignKey(
+ name: "FK_RefreshTokens_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Tokens_RefreshTokenId",
+ table: "Tokens",
+ column: "RefreshTokenId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RefreshTokens_UserId",
+ table: "RefreshTokens",
+ column: "UserId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Tokens_RefreshTokens_RefreshTokenId",
+ table: "Tokens",
+ column: "RefreshTokenId",
+ principalTable: "RefreshTokens",
+ principalColumn: "Id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Tokens_RefreshTokens_RefreshTokenId",
+ table: "Tokens");
+
+ migrationBuilder.DropTable(
+ name: "RefreshTokens");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Tokens_RefreshTokenId",
+ table: "Tokens");
+
+ migrationBuilder.DropColumn(
+ name: "CreationDate",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "RefreshTokenId",
+ table: "Tokens");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.Designer.cs b/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.Designer.cs
new file mode 100644
index 0000000..994f2e8
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.Designer.cs
@@ -0,0 +1,271 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918170412_AddGenuineNpTicketToTokens")]
+ partial class AddGenuineNpTicketToTokens
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasMaxLength(39)
+ .HasColumnType("character varying(39)");
+
+ b.Property("OneTimeUse")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Ips");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GenuineNpTicket")
+ .HasColumnType("boolean");
+
+ b.Property("IpId")
+ .HasColumnType("integer");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("RefreshTokenId")
+ .HasColumnType("uuid");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IpId");
+
+ b.HasIndex("RefreshTokenId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowIpAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowPsnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("AllowRpcnAuthentication")
+ .HasColumnType("boolean");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbIp", "Ip")
+ .WithMany("Tokens")
+ .HasForeignKey("IpId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbRefreshToken", "RefreshToken")
+ .WithMany("Tokens")
+ .HasForeignKey("RefreshTokenId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Ip");
+
+ b.Navigation("RefreshToken");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Navigation("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Navigation("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.cs b/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.cs
new file mode 100644
index 0000000..fdc43e9
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918170412_AddGenuineNpTicketToTokens.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class AddGenuineNpTicketToTokens : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "GenuineNpTicket",
+ table: "Tokens",
+ type: "boolean",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "GenuineNpTicket",
+ table: "Tokens");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.Designer.cs b/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.Designer.cs
new file mode 100644
index 0000000..6ff35a9
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.Designer.cs
@@ -0,0 +1,271 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918171642_RenameGameAuthNames")]
+ partial class RenameGameAuthNames
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .IsRequired()
+ .HasMaxLength(39)
+ .HasColumnType("character varying(39)");
+
+ b.Property("OneTimeUse")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Ips");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GenuineNpTicket")
+ .HasColumnType("boolean");
+
+ b.Property("IpId")
+ .HasColumnType("integer");
+
+ b.Property("Platform")
+ .HasColumnType("integer");
+
+ b.Property("RefreshTokenId")
+ .HasColumnType("uuid");
+
+ b.Property("TokenType")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IpId");
+
+ b.HasIndex("RefreshTokenId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailAddress")
+ .HasMaxLength(320)
+ .HasColumnType("character varying(320)");
+
+ b.Property("FinishedRegistration")
+ .HasColumnType("boolean");
+
+ b.Property("IpAuthorization")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("PasswordBcrypt")
+ .HasMaxLength(60)
+ .HasColumnType("character varying(60)");
+
+ b.Property("PsnAuthorization")
+ .HasColumnType("boolean");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("RpcnAuthorization")
+ .HasColumnType("boolean");
+
+ b.Property("VerifiedEmail")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbToken", b =>
+ {
+ b.HasOne("SoundShapesServer.Types.Database.DbIp", "Ip")
+ .WithMany("Tokens")
+ .HasForeignKey("IpId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbRefreshToken", "RefreshToken")
+ .WithMany("Tokens")
+ .HasForeignKey("RefreshTokenId");
+
+ b.HasOne("SoundShapesServer.Types.Database.DbUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Ip");
+
+ b.Navigation("RefreshToken");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Navigation("Tokens");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b =>
+ {
+ b.Navigation("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.cs b/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.cs
new file mode 100644
index 0000000..47d0e04
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918171642_RenameGameAuthNames.cs
@@ -0,0 +1,48 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ ///
+ public partial class RenameGameAuthNames : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "AllowRpcnAuthentication",
+ table: "Users",
+ newName: "RpcnAuthorization");
+
+ migrationBuilder.RenameColumn(
+ name: "AllowPsnAuthentication",
+ table: "Users",
+ newName: "PsnAuthorization");
+
+ migrationBuilder.RenameColumn(
+ name: "AllowIpAuthentication",
+ table: "Users",
+ newName: "IpAuthorization");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "RpcnAuthorization",
+ table: "Users",
+ newName: "AllowRpcnAuthentication");
+
+ migrationBuilder.RenameColumn(
+ name: "PsnAuthorization",
+ table: "Users",
+ newName: "AllowPsnAuthentication");
+
+ migrationBuilder.RenameColumn(
+ name: "IpAuthorization",
+ table: "Users",
+ newName: "AllowIpAuthentication");
+ }
+ }
+}
diff --git a/SoundShapesServer/Migrations/20240918183007_Shit.Designer.cs b/SoundShapesServer/Migrations/20240918183007_Shit.Designer.cs
new file mode 100644
index 0000000..e11330f
--- /dev/null
+++ b/SoundShapesServer/Migrations/20240918183007_Shit.Designer.cs
@@ -0,0 +1,271 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using SoundShapesServer.Database;
+
+#nullable disable
+
+namespace SoundShapesServer.Migrations
+{
+ [DbContext(typeof(GameDatabaseContext))]
+ [Migration("20240918183007_Shit")]
+ partial class Shit
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(6)
+ .HasColumnType("character varying(6)");
+
+ b.Property("CodeType")
+ .HasColumnType("integer");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Codes");
+ });
+
+ modelBuilder.Entity("SoundShapesServer.Types.Database.DbIp", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Authorized")
+ .HasColumnType("boolean");
+
+ b.Property("AuthorizedDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property