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("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/20240918183007_Shit.cs b/SoundShapesServer/Migrations/20240918183007_Shit.cs new file mode 100644 index 0000000..2f606ec --- /dev/null +++ b/SoundShapesServer/Migrations/20240918183007_Shit.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SoundShapesServer.Migrations +{ + /// + public partial class Shit : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.Designer.cs b/SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.Designer.cs new file mode 100644 index 0000000..e486faf --- /dev/null +++ b/SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.Designer.cs @@ -0,0 +1,272 @@ +// +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("20240918183414_CascadeDeletionRefreshToken")] + partial class CascadeDeletionRefreshToken + { + /// + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20240918183414_CascadeDeletionRefreshToken.cs b/SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.cs new file mode 100644 index 0000000..aa23611 --- /dev/null +++ b/SoundShapesServer/Migrations/20240918183414_CascadeDeletionRefreshToken.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SoundShapesServer.Migrations +{ + /// + public partial class CascadeDeletionRefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tokens_RefreshTokens_RefreshTokenId", + table: "Tokens"); + + migrationBuilder.AddForeignKey( + name: "FK_Tokens_RefreshTokens_RefreshTokenId", + table: "Tokens", + column: "RefreshTokenId", + principalTable: "RefreshTokens", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tokens_RefreshTokens_RefreshTokenId", + table: "Tokens"); + + migrationBuilder.AddForeignKey( + name: "FK_Tokens_RefreshTokens_RefreshTokenId", + table: "Tokens", + column: "RefreshTokenId", + principalTable: "RefreshTokens", + principalColumn: "Id"); + } + } +} diff --git a/SoundShapesServer/Migrations/GameDatabaseContextModelSnapshot.cs b/SoundShapesServer/Migrations/GameDatabaseContextModelSnapshot.cs index af75439..f34e85c 100644 --- a/SoundShapesServer/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/SoundShapesServer/Migrations/GameDatabaseContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("SoundShapesServer.Common.Types.Database.DbCode", b => + modelBuilder.Entity("SoundShapesServer.Types.Database.DbCode", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -54,7 +54,64 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Codes"); }); - modelBuilder.Entity("SoundShapesServer.Common.Types.Database.DbToken", b => + 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() @@ -66,9 +123,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); @@ -77,17 +143,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IpId"); + + b.HasIndex("RefreshTokenId"); + b.HasIndex("UserId"); b.ToTable("Tokens"); }); - modelBuilder.Entity("SoundShapesServer.Common.Types.Database.DbUser", b => + 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)"); @@ -95,6 +168,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("FinishedRegistration") .HasColumnType("boolean"); + b.Property("IpAuthorization") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() .HasMaxLength(16) @@ -104,9 +180,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -115,9 +197,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); - modelBuilder.Entity("SoundShapesServer.Common.Types.Database.DbCode", b => + 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.Common.Types.Database.DbUser", "User") + b.HasOne("SoundShapesServer.Types.Database.DbUser", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -126,9 +219,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("SoundShapesServer.Common.Types.Database.DbToken", b => + modelBuilder.Entity("SoundShapesServer.Types.Database.DbRefreshToken", b => { - b.HasOne("SoundShapesServer.Common.Types.Database.DbUser", "User") + b.HasOne("SoundShapesServer.Types.Database.DbUser", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -136,6 +229,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/Types/Database/DbIp.cs b/SoundShapesServer/Types/Database/DbIp.cs new file mode 100644 index 0000000..4f281d0 --- /dev/null +++ b/SoundShapesServer/Types/Database/DbIp.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace SoundShapesServer.Types.Database; + +/// +/// Used for IP authentication +/// +[PrimaryKey(nameof(Id))] +public class DbIp +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; init; } + public required Guid UserId { get; init; } + public DbUser User { get; init; } = null!; + + [MaxLength(39)] + public required string IpAddress { get; init; } + public required DateTimeOffset CreationDate { get; init; } + public DateTimeOffset AuthorizedDate { get; set; } + public bool Authorized { get; init; } + public bool OneTimeUse { get; set; } + public ICollection Tokens { get; set; } = null!; +} \ No newline at end of file diff --git a/SoundShapesServer/Types/Database/DbRefreshToken.cs b/SoundShapesServer/Types/Database/DbRefreshToken.cs new file mode 100644 index 0000000..9e8da62 --- /dev/null +++ b/SoundShapesServer/Types/Database/DbRefreshToken.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace SoundShapesServer.Types.Database; + +[PrimaryKey(nameof(Id))] +public class DbRefreshToken +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; init; } + + [ForeignKey(nameof(User))] + public required Guid UserId { get; init; } + + public DbUser User { get; init; } = null!; + + public ICollection Tokens { get; set; } + + public required DateTimeOffset CreationDate { get; init; } + public required DateTimeOffset ExpiryDate { get; init; } +} \ No newline at end of file diff --git a/SoundShapesServer/Types/Database/DbToken.cs b/SoundShapesServer/Types/Database/DbToken.cs index c9d335b..3cf4ff6 100644 --- a/SoundShapesServer/Types/Database/DbToken.cs +++ b/SoundShapesServer/Types/Database/DbToken.cs @@ -22,4 +22,12 @@ public class DbToken : IToken public required TokenType TokenType { get; set; } public required PlatformType? Platform { get; set; } + public required bool? GenuineNpTicket { get; set; } + + public required int? IpId { get; set; } + public DbIp? Ip { get; set; } + + [ForeignKey(nameof(RefreshToken))] + public required Guid? RefreshTokenId { get; set; } + public DbRefreshToken? RefreshToken { get; set; } } \ No newline at end of file diff --git a/SoundShapesServer/Types/Database/DbUser.cs b/SoundShapesServer/Types/Database/DbUser.cs index 595a2af..e107fe2 100644 --- a/SoundShapesServer/Types/Database/DbUser.cs +++ b/SoundShapesServer/Types/Database/DbUser.cs @@ -27,6 +27,12 @@ public class DbUser: IUser [MinLength(60)] public string? PasswordBcrypt { get; set; } + public bool RpcnAuthorization { get; set; } = false; + public bool PsnAuthorization { get; set; } = false; + public bool IpAuthorization { get; set; } = false; + + public required DateTimeOffset CreationDate { get; set; } + public override string ToString() { string result = $"{this.Name} ({this.Role}"; diff --git a/SoundShapesServer/Types/Requests/Api/ApiRefreshTokenRequest.cs b/SoundShapesServer/Types/Requests/Api/ApiRefreshTokenRequest.cs new file mode 100644 index 0000000..9bd8f3c --- /dev/null +++ b/SoundShapesServer/Types/Requests/Api/ApiRefreshTokenRequest.cs @@ -0,0 +1,6 @@ +namespace SoundShapesServer.Types.Requests.Api; + +public record ApiRefreshTokenRequest : IApiRequest +{ + public required Guid RefreshTokenId { get; init; } +} \ No newline at end of file diff --git a/SoundShapesServer/Types/Responses/Api/ApiTypes/ApiResponse.cs b/SoundShapesServer/Types/Responses/Api/ApiTypes/ApiResponse.cs index 845b845..ba7faaf 100644 --- a/SoundShapesServer/Types/Responses/Api/ApiTypes/ApiResponse.cs +++ b/SoundShapesServer/Types/Responses/Api/ApiTypes/ApiResponse.cs @@ -6,6 +6,12 @@ namespace SoundShapesServer.Types.Responses.Api.ApiTypes; public class ApiResponse : IHasResponseCode where T : class { + [Obsolete("Only used for deserialization")] + public ApiResponse() + { + + } + protected ApiResponse(T data) { Success = true; diff --git a/SoundShapesServer/Types/Responses/Api/ApiTypes/Errors/ApiUnauthorizedError.cs b/SoundShapesServer/Types/Responses/Api/ApiTypes/Errors/ApiUnauthorizedError.cs index 0877365..39acde4 100644 --- a/SoundShapesServer/Types/Responses/Api/ApiTypes/Errors/ApiUnauthorizedError.cs +++ b/SoundShapesServer/Types/Responses/Api/ApiTypes/Errors/ApiUnauthorizedError.cs @@ -13,7 +13,9 @@ public class ApiUnauthorizedError : ApiError public const string EulaNotAcceptedWhen = "You have to accept the eula to register an account."; public static readonly ApiUnauthorizedError EulaNotAccepted = new(EulaNotAcceptedWhen); public const string InvalidCodeWhen = "Invalid code."; - public static readonly ApiUnauthorizedError InvalidCode = new(InvalidCodeWhen); + public static readonly ApiUnauthorizedError InvalidCode = new(InvalidCodeWhen); + public const string InvalidEmailOrPasswordWhen = "Invalid e-mail or password."; + public static readonly ApiUnauthorizedError InvalidEmailOrPassword = new(InvalidEmailOrPasswordWhen); private ApiUnauthorizedError(string message) : base(message, HttpStatusCode.Unauthorized) { diff --git a/SoundShapesServer/Types/Responses/Api/DataTypes/ApiLoginResponse.cs b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiLoginResponse.cs new file mode 100644 index 0000000..09a7447 --- /dev/null +++ b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiLoginResponse.cs @@ -0,0 +1,10 @@ +using SoundShapesServer.Types.Responses.Api.ApiTypes; + +namespace SoundShapesServer.Types.Responses.Api.DataTypes; + +public record ApiLoginResponse : IApiResponse +{ + public required ApiFullUserResponse User { get; set; } + public required ApiTokenResponse AccessToken { get; set; } + public required ApiRefreshTokenResponse RefreshToken { get; set; } +} \ No newline at end of file diff --git a/SoundShapesServer/Types/Responses/Api/DataTypes/ApiRefreshTokenResponse.cs b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiRefreshTokenResponse.cs new file mode 100644 index 0000000..ca0709e --- /dev/null +++ b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiRefreshTokenResponse.cs @@ -0,0 +1,20 @@ +using SoundShapesServer.Types.Database; +using SoundShapesServer.Types.Responses.Api.ApiTypes; + +namespace SoundShapesServer.Types.Responses.Api.DataTypes; + +public record ApiRefreshTokenResponse : IApiDbResponse +{ + public required Guid Id { get; set; } + public required DateTimeOffset CreationDate { get; set; } + public required DateTimeOffset ExpiryDate { get; set; } + public static ApiRefreshTokenResponse FromDb(DbRefreshToken value) + { + return new ApiRefreshTokenResponse + { + Id = value.Id, + CreationDate = value.CreationDate, + ExpiryDate = value.ExpiryDate + }; + } +} \ No newline at end of file diff --git a/SoundShapesServer/Types/Responses/Api/DataTypes/ApiTokenResponse.cs b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiTokenResponse.cs new file mode 100644 index 0000000..4e6e6b7 --- /dev/null +++ b/SoundShapesServer/Types/Responses/Api/DataTypes/ApiTokenResponse.cs @@ -0,0 +1,23 @@ +using SoundShapesServer.Types.Database; +using SoundShapesServer.Types.Responses.Api.ApiTypes; + +namespace SoundShapesServer.Types.Responses.Api.DataTypes; + +public record ApiTokenResponse : IApiDbResponse +{ + public required Guid Id { get; set; } + public required DateTimeOffset CreationDate { get; set; } + public required DateTimeOffset ExpiryDate { get; set; } + public required TokenType TokenType { get; set; } + + public static ApiTokenResponse FromDb(DbToken value) + { + return new ApiTokenResponse + { + Id = value.Id, + CreationDate = value.CreationDate, + ExpiryDate = value.ExpiryDate, + TokenType = value.TokenType + }; + } +} \ No newline at end of file