From 8877472c87c9a9c9f4dc6775e2e405f6be57ca03 Mon Sep 17 00:00:00 2001 From: Dmytro Kyshchenko Date: Sun, 17 Nov 2024 18:27:04 +0200 Subject: [PATCH] #269. Expire passwords. --- Pyro.Api/Directory.Packages.props | 1 + Pyro.Api/Pyro.ApiTests/Clients/PyroClient.cs | 2 + Pyro.Api/Pyro.ApiTests/Tests/ProfileTests.cs | 1 - .../UpdateUserProfileRequest.cs | 2 +- .../{ => Identity}/UserProfileResponse.cs | 2 +- .../Commands/LockUserHandlerTests.cs | 13 +- .../Commands/LoginHandlerTests.cs | 58 +- .../Commands/RefreshTokenHandlerTests.cs | 63 +- .../Commands/UpdateUserHandlerTests.cs | 12 +- .../Models/UserTests.cs | 120 +++- .../PasswordServiceTests.cs | 6 +- .../Commands/ActivateUser.cs | 3 +- .../Commands/ChangePassword.cs | 5 +- .../Commands/LoginCommand.cs | 10 + .../Commands/NotifyExpiringPasswords.cs | 61 ++ .../Commands/RefreshToken.cs | 7 + .../Commands}/UpdateProfile.cs | 14 +- .../Pyro.Domain.Identity/IUserRepository.cs | 2 + Pyro.Api/Pyro.Domain.Identity/Models/User.cs | 36 +- .../Models}/UserProfile.cs | 8 +- .../Queries}/GetUserProfile.cs | 14 +- .../Email/EmailServiceOptions.cs | 2 +- .../Pyro.Domain.Shared.csproj | 1 + .../UserProfiles/CreateUserProfile.cs | 40 -- .../UserProfiles/IUserProfileRepository.cs | 11 - Pyro.Api/Pyro.Domain/UserProfiles/User.cs | 6 - .../Pyro.Domain/UserProfiles/UserAvatar.cs | 13 - .../DataAccess/Configurations/SeedData.cs | 12 + .../Configurations/UserConfiguration.cs | 11 + .../UserProfileConfiguration.cs | 13 +- .../DataAccess/UserRepository.cs | 19 +- .../20240812212140_Initial.Designer.cs | 419 ------------- ...20241028213426_UpdatePyroLogin.Designer.cs | 419 ------------- .../20241028213426_UpdatePyroLogin.cs | 34 - .../20241106114633_AddOneTimePasswords.cs | 69 -- ....cs => 20241118140123_Initial.Designer.cs} | 49 +- ...0_Initial.cs => 20241118140123_Initial.cs} | 67 +- .../IdentityDbContextModelSnapshot.cs | 45 +- .../20240812212127_Initial.Designer.cs | 214 ------- .../Migrations/20240812212127_Initial.cs | 132 ---- .../20240818120513_AddTags.Designer.cs | 288 --------- .../Migrations/20240818120513_AddTags.cs | 170 ----- ...40819170437_RenameTagsToLabels.Designer.cs | 293 --------- .../20240819170437_RenameTagsToLabels.cs | 80 --- .../20240902163715_AddStatuses.Designer.cs | 395 ------------ .../Migrations/20240902163715_AddStatuses.cs | 121 ---- .../20240908182802_AddIsLocked.Designer.cs | 400 ------------ .../Migrations/20240908182802_AddIsLocked.cs | 30 - ...40917153755_AddIssueChangeLogs.Designer.cs | 593 ------------------ ...1011095604_UpdateIssueStatusUniqueIndex.cs | 48 -- ....cs => 20241118140145_Initial.Designer.cs} | 6 +- ...hangeLogs.cs => 20241118140145_Initial.cs} | 229 ++++++- .../IssuesDbContextModelSnapshot.cs | 2 +- .../DataAccess/Configurations/SeedData.cs | 23 - .../UserProfiles/UserAvatarConfiguration.cs | 25 - .../UserProfiles/UserConfiguration.cs | 24 - .../DataAccess/UserProfileRepository.cs | 33 - Pyro.Api/Pyro.Infrastructure/GitService.cs | 14 +- Pyro.Api/Pyro.Infrastructure/Messaging/Bus.cs | 1 + .../20240818120634_AddTags.Designer.cs | 207 ------ .../Migrations/20240818120634_AddTags.cs | 54 -- ...40819170348_RenameTagsToLabels.Designer.cs | 207 ------ .../20240819170348_RenameTagsToLabels.cs | 88 --- ...6092147_AddIsEnabledIsDisabled.Designer.cs | 212 ------- .../20240916092147_AddIsEnabledIsDisabled.cs | 30 - ...006_UpdateLabelNameUniqueIndex.Designer.cs | 210 ------- ...241010161006_UpdateLabelNameUniqueIndex.cs | 48 -- ...0241028213258_RemoveEmailField.Designer.cs | 236 ------- .../20241028213258_RemoveEmailField.cs | 38 -- ....cs => 20241118140058_Initial.Designer.cs} | 60 +- ...1_Initial.cs => 20241118140058_Initial.cs} | 45 +- .../Migrations/PyroDbContextModelSnapshot.cs | 89 +-- .../ServiceCollectionExtensions.cs | 4 +- ...otifyExpiringPasswordsBackgroundService.cs | 53 ++ .../RemoveExpiredOneTimePasswords.cs | 1 + .../DomainEventHandlers/UserCreatedHandler.cs | 23 - Pyro.Api/Pyro/DtoMappings/DtoMapper.cs | 9 - Pyro.Api/Pyro/DtoMappings/IdentityMapper.cs | 7 + Pyro.Api/Pyro/Endpoints/IdentityEndpoints.cs | 48 +- Pyro.Api/Pyro/Endpoints/ProfileEndpoints.cs | 59 -- Pyro.Api/Pyro/Extensions/LoggingExtensions.cs | 19 + .../UserCreatedIntegrationEventHandler.cs | 11 +- Pyro.Api/Pyro/Program.cs | 3 +- Pyro.Api/Pyro/ServiceCollectionExtensions.cs | 7 +- .../Services/BasicAuthenticationHandler.cs | 7 + Pyro.Api/Pyro/Services/GitBackend.cs | 16 +- Pyro.Api/Pyro/Services/LoggerProvider.cs | 55 ++ Pyro.Api/Pyro/appsettings.json | 3 + .../profile-edit/profile-edit.component.html | 11 - .../profile-edit/profile-edit.component.ts | 11 - Pyro.UI/src/app/services/profile.service.ts | 1 - 91 files changed, 1047 insertions(+), 5616 deletions(-) rename Pyro.Api/Pyro.Contracts/Requests/{ => Identity}/UpdateUserProfileRequest.cs (84%) rename Pyro.Api/Pyro.Contracts/Responses/{ => Identity}/UserProfileResponse.cs (83%) create mode 100644 Pyro.Api/Pyro.Domain.Identity/Commands/NotifyExpiringPasswords.cs rename Pyro.Api/{Pyro.Domain/UserProfiles => Pyro.Domain.Identity/Commands}/UpdateProfile.cs (73%) rename Pyro.Api/{Pyro.Domain/UserProfiles => Pyro.Domain.Identity/Models}/UserProfile.cs (59%) rename Pyro.Api/{Pyro.Domain/UserProfiles => Pyro.Domain.Identity/Queries}/GetUserProfile.cs (69%) rename Pyro.Api/{Pyro.Infrastructure.Shared => Pyro.Domain.Shared}/Email/EmailServiceOptions.cs (97%) delete mode 100644 Pyro.Api/Pyro.Domain/UserProfiles/CreateUserProfile.cs delete mode 100644 Pyro.Api/Pyro.Domain/UserProfiles/IUserProfileRepository.cs delete mode 100644 Pyro.Api/Pyro.Domain/UserProfiles/User.cs delete mode 100644 Pyro.Api/Pyro.Domain/UserProfiles/UserAvatar.cs rename Pyro.Api/{Pyro.Infrastructure/DataAccess/Configurations/UserProfiles => Pyro.Infrastructure.Identity/DataAccess/Configurations}/UserProfileConfiguration.cs (68%) delete mode 100644 Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.cs rename Pyro.Api/Pyro.Infrastructure.Identity/Migrations/{20241106114633_AddOneTimePasswords.Designer.cs => 20241118140123_Initial.Designer.cs} (91%) rename Pyro.Api/Pyro.Infrastructure.Identity/Migrations/{20240812212140_Initial.cs => 20241118140123_Initial.cs} (79%) delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.cs rename Pyro.Api/Pyro.Infrastructure.Issues/Migrations/{20241011095604_UpdateIssueStatusUniqueIndex.Designer.cs => 20241118140145_Initial.Designer.cs} (99%) rename Pyro.Api/Pyro.Infrastructure.Issues/Migrations/{20240917153755_AddIssueChangeLogs.cs => 20241118140145_Initial.cs} (55%) delete mode 100644 Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/SeedData.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserAvatarConfiguration.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserConfiguration.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/DataAccess/UserProfileRepository.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.Designer.cs delete mode 100644 Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.cs rename Pyro.Api/Pyro.Infrastructure/Migrations/{20240812212151_Initial.Designer.cs => 20241118140058_Initial.Designer.cs} (69%) rename Pyro.Api/Pyro.Infrastructure/Migrations/{20240812212151_Initial.cs => 20241118140058_Initial.cs} (74%) create mode 100644 Pyro.Api/Pyro/BackgroundServices/NotifyExpiringPasswordsBackgroundService.cs delete mode 100644 Pyro.Api/Pyro/DomainEventHandlers/UserCreatedHandler.cs delete mode 100644 Pyro.Api/Pyro/Endpoints/ProfileEndpoints.cs create mode 100644 Pyro.Api/Pyro/Extensions/LoggingExtensions.cs create mode 100644 Pyro.Api/Pyro/Services/LoggerProvider.cs diff --git a/Pyro.Api/Directory.Packages.props b/Pyro.Api/Directory.Packages.props index 4b046cd7..25e9c784 100644 --- a/Pyro.Api/Directory.Packages.props +++ b/Pyro.Api/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/Pyro.Api/Pyro.ApiTests/Clients/PyroClient.cs b/Pyro.Api/Pyro.ApiTests/Clients/PyroClient.cs index e7b7289a..2244a396 100644 --- a/Pyro.Api/Pyro.ApiTests/Clients/PyroClient.cs +++ b/Pyro.Api/Pyro.ApiTests/Clients/PyroClient.cs @@ -2,8 +2,10 @@ // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. using Pyro.Contracts.Requests; +using Pyro.Contracts.Requests.Identity; using Pyro.Contracts.Requests.Issues; using Pyro.Contracts.Responses; +using Pyro.Contracts.Responses.Identity; namespace Pyro.ApiTests.Clients; diff --git a/Pyro.Api/Pyro.ApiTests/Tests/ProfileTests.cs b/Pyro.Api/Pyro.ApiTests/Tests/ProfileTests.cs index 2afa433d..01b7b1b4 100644 --- a/Pyro.Api/Pyro.ApiTests/Tests/ProfileTests.cs +++ b/Pyro.Api/Pyro.ApiTests/Tests/ProfileTests.cs @@ -3,7 +3,6 @@ using Bogus; using Pyro.ApiTests.Clients; -using Pyro.Contracts.Requests; using Pyro.Contracts.Requests.Identity; namespace Pyro.ApiTests.Tests; diff --git a/Pyro.Api/Pyro.Contracts/Requests/UpdateUserProfileRequest.cs b/Pyro.Api/Pyro.Contracts/Requests/Identity/UpdateUserProfileRequest.cs similarity index 84% rename from Pyro.Api/Pyro.Contracts/Requests/UpdateUserProfileRequest.cs rename to Pyro.Api/Pyro.Contracts/Requests/Identity/UpdateUserProfileRequest.cs index 2981cfc2..422f1718 100644 --- a/Pyro.Api/Pyro.Contracts/Requests/UpdateUserProfileRequest.cs +++ b/Pyro.Api/Pyro.Contracts/Requests/Identity/UpdateUserProfileRequest.cs @@ -1,6 +1,6 @@ // Copyright (c) Dmytro Kyshchenko. All rights reserved. // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. -namespace Pyro.Contracts.Requests; +namespace Pyro.Contracts.Requests.Identity; public record UpdateUserProfileRequest(string Name, string Status); \ No newline at end of file diff --git a/Pyro.Api/Pyro.Contracts/Responses/UserProfileResponse.cs b/Pyro.Api/Pyro.Contracts/Responses/Identity/UserProfileResponse.cs similarity index 83% rename from Pyro.Api/Pyro.Contracts/Responses/UserProfileResponse.cs rename to Pyro.Api/Pyro.Contracts/Responses/Identity/UserProfileResponse.cs index 384f37db..aced62d0 100644 --- a/Pyro.Api/Pyro.Contracts/Responses/UserProfileResponse.cs +++ b/Pyro.Api/Pyro.Contracts/Responses/Identity/UserProfileResponse.cs @@ -1,6 +1,6 @@ // Copyright (c) Dmytro Kyshchenko. All rights reserved. // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. -namespace Pyro.Contracts.Responses; +namespace Pyro.Contracts.Responses.Identity; public record UserProfileResponse(string Name, string? Status); \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LockUserHandlerTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LockUserHandlerTests.cs index e46c4261..b39a8c16 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LockUserHandlerTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LockUserHandlerTests.cs @@ -27,7 +27,12 @@ public void LockMissingUser() public void LockCurrentUser() { var currentUser = new CurrentUser(Guid.NewGuid(), "user", [], []); - var user = new User { Id = currentUser.Id, Login = currentUser.Login }; + var user = new User + { + Id = currentUser.Id, + Login = currentUser.Login, + Profile = new UserProfile { Name = currentUser.Login }, + }; var command = new LockUser(currentUser.Login); var userRepository = Substitute.For(); @@ -48,7 +53,11 @@ public void LockCurrentUser() public async Task LockUser() { var currentUser = new CurrentUser(Guid.NewGuid(), "user", [], []); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var command = new LockUser(user.Login); var userRepository = Substitute.For(); diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LoginHandlerTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LoginHandlerTests.cs index 59862b2e..b9c5f2a4 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LoginHandlerTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/LoginHandlerTests.cs @@ -16,10 +16,11 @@ public async Task LoginWithInvalidUser() var command = new LoginCommand("test", "password"); var logger = Substitute.For>(); + var timeProvider = Substitute.For(); var repository = Substitute.For(); var passwordService = Substitute.For(); var tokenService = Substitute.For(); - var handler = new LoginHandler(logger, repository, passwordService, tokenService); + var handler = new LoginHandler(logger, timeProvider, repository, passwordService, tokenService); var result = await handler.Handle(command); @@ -30,17 +31,50 @@ public async Task LoginWithInvalidUser() public async Task LoginWithLockedUser() { var command = new LoginCommand("test", "password"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.Lock(); var logger = Substitute.For>(); + var timeProvider = Substitute.For(); var repository = Substitute.For(); repository .GetUserByLogin(command.Login) .Returns(user); var passwordService = Substitute.For(); var tokenService = Substitute.For(); - var handler = new LoginHandler(logger, repository, passwordService, tokenService); + var handler = new LoginHandler(logger, timeProvider, repository, passwordService, tokenService); + + var result = await handler.Handle(command); + + Assert.That(result, Is.EqualTo(LoginResult.Fail())); + } + + [Test] + public async Task LoginWithExpiredPassword() + { + var currentDate = DateTimeOffset.UtcNow; + var command = new LoginCommand("test", "password"); + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + PasswordExpiresAt = currentDate.AddDays(-1), + }; + + var logger = Substitute.For>(); + var timeProvider = Substitute.For(); + timeProvider.GetUtcNow().Returns(currentDate); + var repository = Substitute.For(); + repository + .GetUserByLogin(command.Login) + .Returns(user); + var passwordService = Substitute.For(); + var tokenService = Substitute.For(); + var handler = new LoginHandler(logger, timeProvider, repository, passwordService, tokenService); var result = await handler.Handle(command); @@ -51,9 +85,14 @@ public async Task LoginWithLockedUser() public async Task LoginWithInvalidCredentials() { var command = new LoginCommand("test", "password"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var logger = Substitute.For>(); + var timeProvider = Substitute.For(); var repository = Substitute.For(); repository .GetUserByLogin(command.Login) @@ -63,7 +102,7 @@ public async Task LoginWithInvalidCredentials() .VerifyPassword(command.Password, user.Password, user.Salt) .Returns(false); var tokenService = Substitute.For(); - var handler = new LoginHandler(logger, repository, passwordService, tokenService); + var handler = new LoginHandler(logger, timeProvider, repository, passwordService, tokenService); var result = await handler.Handle(command); @@ -74,12 +113,17 @@ public async Task LoginWithInvalidCredentials() public async Task LoginWithValidCredentials() { var command = new LoginCommand("test", "password"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var jwtTokenPair = new JwtTokenPair( new Token(Guid.NewGuid(), "access", DateTimeOffset.UtcNow), new Token(Guid.NewGuid(), "refresh", DateTimeOffset.UtcNow)); var logger = Substitute.For>(); + var timeProvider = Substitute.For(); var repository = Substitute.For(); repository .GetUserByLogin(command.Login) @@ -92,7 +136,7 @@ public async Task LoginWithValidCredentials() tokenService .GenerateTokenPair(user) .Returns(jwtTokenPair); - var handler = new LoginHandler(logger, repository, passwordService, tokenService); + var handler = new LoginHandler(logger, timeProvider, repository, passwordService, tokenService); var result = await handler.Handle(command); diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/RefreshTokenHandlerTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/RefreshTokenHandlerTests.cs index ddc907bf..8b4b8861 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/RefreshTokenHandlerTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/RefreshTokenHandlerTests.cs @@ -41,7 +41,11 @@ public async Task RefreshTokenWithInvalidUser() public async Task RefreshTokenWithLockedUser() { var command = new RefreshToken("token"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.Lock(); var jwtToken = new JwtToken @@ -70,11 +74,53 @@ public async Task RefreshTokenWithLockedUser() Assert.That(result, Is.EqualTo(RefreshTokenResult.Fail())); } + [Test] + public async Task RefreshTokenWithExpiredPassword() + { + var currentDate = DateTimeOffset.UtcNow; + var command = new RefreshToken("token"); + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + PasswordExpiresAt = currentDate.AddDays(-1), + }; + var jwtToken = new JwtToken + { + TokenId = Guid.NewGuid(), + IssuedAt = 0, + ExpiresAt = 0, + UserId = user.Id, + Login = user.Login, + }; + + var logger = Substitute.For>(); + var userRepository = Substitute.For(); + userRepository + .GetUserById(jwtToken.UserId) + .Returns(user); + var tokenService = Substitute.For(); + tokenService + .DecodeTokenId(command.Token) + .Returns(jwtToken); + var timeProvider = Substitute.For(); + timeProvider.GetUtcNow().Returns(currentDate); + var handler = new RefreshTokenHandler(logger, userRepository, tokenService, timeProvider); + + var result = await handler.Handle(command); + + Assert.That(result, Is.EqualTo(RefreshTokenResult.Fail())); + } + [Test] public async Task RefreshTokenWithoutAuthToken() { var command = new RefreshToken("token"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var jwtToken = new JwtToken { TokenId = Guid.NewGuid(), @@ -106,7 +152,11 @@ public async Task RefreshTokenWithExpiredToken() { var currentDate = new DateTimeOffset(new DateTime(2024, 01, 01)); var command = new RefreshToken("token"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var authToken = AuthenticationToken.Create(Guid.NewGuid(), user, currentDate.AddMonths(-1)); user.AddAuthenticationToken(authToken); var jwtToken = new JwtToken @@ -143,7 +193,12 @@ public async Task RefreshToken() { var currentDate = new DateTimeOffset(new DateTime(2024, 01, 01)); var command = new RefreshToken("token"); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + PasswordExpiresAt = currentDate.AddDays(90), + }; var authToken = AuthenticationToken.Create(Guid.NewGuid(), user, currentDate.AddMonths(1)); user.AddAuthenticationToken(authToken); var jwtToken = new JwtToken diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/UpdateUserHandlerTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/UpdateUserHandlerTests.cs index b631736d..acf444af 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/UpdateUserHandlerTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Commands/UpdateUserHandlerTests.cs @@ -13,7 +13,11 @@ public class UpdateUserHandlerTests [Test] public void UpdateUserWithInvalidRole() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var updateUser = new UpdateUser(user, ["admin"]); var repository = Substitute.For(); @@ -33,7 +37,11 @@ public async Task UpdateUser() new Role { Name = "admin" }, new Role { Name = "user" }, }; - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.AddRole(roles[1]); var updateUser = new UpdateUser(user, [roles[0].Name]); diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Models/UserTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Models/UserTests.cs index c5ffc1d8..82d42971 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/Models/UserTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/Models/UserTests.cs @@ -15,7 +15,11 @@ public class UserTests [Test] public void AddRoleToUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var role = new Role { Name = "Test Role", @@ -28,7 +32,11 @@ public void AddRoleToUser() [Test] public void AddExistingRoleToUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var role = new Role { Name = "Test Role", @@ -43,7 +51,11 @@ public void AddExistingRoleToUser() [Test] public void RemoveAllRolesFromUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var role = new Role { Name = "Test Role", @@ -57,7 +69,11 @@ public void RemoveAllRolesFromUser() [Test] public void AddAuthTokenToUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = AuthenticationToken.Create(Guid.NewGuid(), user, DateTimeOffset.UtcNow); user.AddAuthenticationToken(token); @@ -67,7 +83,11 @@ public void AddAuthTokenToUser() [Test] public void GetAuthTokenFromUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = AuthenticationToken.Create(Guid.NewGuid(), user, DateTimeOffset.UtcNow); user.AddAuthenticationToken(token); var result = user.GetAuthenticationToken(token.TokenId); @@ -78,7 +98,11 @@ public void GetAuthTokenFromUser() [Test] public void AddAccessTokenToUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = new AccessToken { Name = "Test Token", @@ -96,7 +120,11 @@ public void AddAccessTokenToUser() [Test] public void DeleteAccessTokenFromUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = new AccessToken { Name = "Test Token", @@ -114,7 +142,11 @@ public void DeleteAccessTokenFromUser() public void ValidateAccessToken() { var currentDate = new DateTimeOffset(new DateTime(2024, 01, 01)); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = new AccessToken { Name = "Test Token", @@ -139,7 +171,11 @@ public void ValidateAccessToken() public void ValidateExpiredAccessToken() { var currentDate = new DateTimeOffset(new DateTime(2024, 01, 01)); - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var token = new AccessToken { Name = "Test Token", @@ -163,7 +199,11 @@ public void ValidateExpiredAccessToken() [Test] public void LockUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.AddAuthenticationToken(new AuthenticationToken()); user.Lock(); @@ -178,7 +218,11 @@ public void LockUser() [Test] public void UnlockUser() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.Unlock(); @@ -188,10 +232,16 @@ public void UnlockUser() [Test] public void ActivateUserIsNull() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; + var timeProvider = Substitute.For(); + var passwordService = Substitute.For(); - Assert.Throws(() => user.Activate(timeProvider, null!, [], [])); + Assert.Throws(() => user.Activate(timeProvider, passwordService, null!, string.Empty)); } [Test] @@ -199,7 +249,11 @@ public void ActivateUserWithExpiredToken() { var currentTime = DateTimeOffset.UtcNow; - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var oneTimePassword = new OneTimePassword { Token = "token", @@ -214,7 +268,9 @@ public void ActivateUserWithExpiredToken() .GetUtcNow() .Returns(currentTime); - Assert.Throws(() => user.Activate(timeProvider, oneTimePassword, [], [])); + var passwordService = Substitute.For(); + + Assert.Throws(() => user.Activate(timeProvider, passwordService, oneTimePassword, string.Empty)); } [Test] @@ -222,7 +278,11 @@ public void ActivateLockedUser() { var currentTime = DateTimeOffset.UtcNow; - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var oneTimePassword = new OneTimePassword { Token = "token", @@ -237,7 +297,9 @@ public void ActivateLockedUser() .GetUtcNow() .Returns(currentTime); - Assert.Throws(() => user.Activate(timeProvider, oneTimePassword, [], [])); + var passwordService = Substitute.For(); + + Assert.Throws(() => user.Activate(timeProvider, passwordService, oneTimePassword, string.Empty)); } [Test] @@ -245,7 +307,11 @@ public void ActivateUser() { var currentTime = DateTimeOffset.UtcNow; - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; user.Lock(); var oneTimePassword = new OneTimePassword @@ -262,7 +328,12 @@ public void ActivateUser() .GetUtcNow() .Returns(currentTime); - user.Activate(timeProvider, oneTimePassword, [], []); + var passwordService = Substitute.For(); + passwordService + .GeneratePasswordHash(Arg.Any()) + .Returns((new byte[64], new byte[16])); + + user.Activate(timeProvider, passwordService, oneTimePassword, "password"); Assert.Multiple(() => { @@ -283,9 +354,10 @@ public void ChangeIncorrectPassword() Login = "test@localhost.local", Password = passwordHash, Salt = salt, + Profile = new UserProfile { Name = "test" }, }; - Assert.Throws(() => user.ChangePassword(passwordService, "incorrect", "newPassword")); + Assert.Throws(() => user.ChangePassword(TimeProvider.System, passwordService, "incorrect", "newPassword")); } [Test] @@ -293,6 +365,10 @@ public void ChangePassword() { const string oldPassword = "password"; + var currentDateTime = DateTimeOffset.UtcNow; + var currentTimeProvider = Substitute.For(); + currentTimeProvider.GetUtcNow().Returns(currentDateTime); + var passwordService = new PasswordService(TimeProvider.System); var (passwordHash, salt) = passwordService.GeneratePasswordHash(oldPassword); var user = new User @@ -300,14 +376,16 @@ public void ChangePassword() Login = "test@localhost.local", Password = passwordHash, Salt = salt, + Profile = new UserProfile { Name = "test" }, }; - user.ChangePassword(passwordService, oldPassword, "newPassword"); + user.ChangePassword(currentTimeProvider, passwordService, oldPassword, "newPassword"); Assert.Multiple(() => { Assert.That(user.Password, Is.Not.EqualTo(passwordHash)); Assert.That(user.Salt, Is.Not.EqualTo(salt)); + Assert.That(user.PasswordExpiresAt, Is.EqualTo(currentDateTime.AddDays(90))); }); } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity.UnitTests/PasswordServiceTests.cs b/Pyro.Api/Pyro.Domain.Identity.UnitTests/PasswordServiceTests.cs index b4278df0..41343e78 100644 --- a/Pyro.Api/Pyro.Domain.Identity.UnitTests/PasswordServiceTests.cs +++ b/Pyro.Api/Pyro.Domain.Identity.UnitTests/PasswordServiceTests.cs @@ -10,7 +10,11 @@ public class PasswordServiceTests [Test] public void GenerateOneTimePasswordForTest() { - var user = new User { Login = "test" }; + var user = new User + { + Login = "test", + Profile = new UserProfile { Name = "test" }, + }; var passwordService = new PasswordService(TimeProvider.System); var otp = passwordService.GenerateOneTimePasswordFor(user); diff --git a/Pyro.Api/Pyro.Domain.Identity/Commands/ActivateUser.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/ActivateUser.cs index a2e96368..492b204b 100644 --- a/Pyro.Api/Pyro.Domain.Identity/Commands/ActivateUser.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/ActivateUser.cs @@ -51,8 +51,7 @@ public async Task Handle(ActivateUser request, CancellationToken cancellationTok return; } - var (passwordHash, salt) = passwordService.GeneratePasswordHash(request.Password); - user.Activate(timeProvider, token, passwordHash, salt); + user.Activate(timeProvider, passwordService, token, request.Password); logger.LogInformation("User '{Login}' activated", user.Login); } diff --git a/Pyro.Api/Pyro.Domain.Identity/Commands/ChangePassword.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/ChangePassword.cs index 4ca50b0b..5e5e1184 100644 --- a/Pyro.Api/Pyro.Domain.Identity/Commands/ChangePassword.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/ChangePassword.cs @@ -25,15 +25,18 @@ public ChangePasswordValidator() public class ChangePasswordHandler : IRequestHandler { + private readonly TimeProvider timeProvider; private readonly ICurrentUserProvider currentUserProvider; private readonly IUserRepository userRepository; private readonly IPasswordService passwordService; public ChangePasswordHandler( + TimeProvider timeProvider, ICurrentUserProvider currentUserProvider, IUserRepository userRepository, IPasswordService passwordService) { + this.timeProvider = timeProvider; this.currentUserProvider = currentUserProvider; this.userRepository = userRepository; this.passwordService = passwordService; @@ -45,6 +48,6 @@ public async Task Handle(ChangePassword request, CancellationToken cancellationT var user = await userRepository.GetUserById(currentUser.Id, cancellationToken) ?? throw new NotFoundException($"The user (Id: {currentUser.Id}) not found"); - user.ChangePassword(passwordService, request.OldPassword, request.NewPassword); + user.ChangePassword(timeProvider, passwordService, request.OldPassword, request.NewPassword); } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity/Commands/LoginCommand.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/LoginCommand.cs index 6407904c..9d964594 100644 --- a/Pyro.Api/Pyro.Domain.Identity/Commands/LoginCommand.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/LoginCommand.cs @@ -64,17 +64,20 @@ public LoginValidator() public class LoginHandler : IRequestHandler { private readonly ILogger logger; + private readonly TimeProvider timeProvider; private readonly IUserRepository repository; private readonly IPasswordService passwordService; private readonly ITokenService tokenService; public LoginHandler( ILogger logger, + TimeProvider timeProvider, IUserRepository repository, IPasswordService passwordService, ITokenService tokenService) { this.logger = logger; + this.timeProvider = timeProvider; this.repository = repository; this.passwordService = passwordService; this.tokenService = tokenService; @@ -97,6 +100,13 @@ public async Task Handle(LoginCommand request, CancellationToken ca return LoginResult.Fail(); } + if (user.PasswordExpiresAt < timeProvider.GetUtcNow()) + { + logger.LogWarning("The password of '{User}' user is expired", user.Login); + + return LoginResult.Fail(); + } + if (!passwordService.VerifyPassword(request.Password, user.Password, user.Salt)) { logger.LogWarning("Login attempt. User with login '{Login}' provided invalid password", request.Login); diff --git a/Pyro.Api/Pyro.Domain.Identity/Commands/NotifyExpiringPasswords.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/NotifyExpiringPasswords.cs new file mode 100644 index 00000000..b2fb1fec --- /dev/null +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/NotifyExpiringPasswords.cs @@ -0,0 +1,61 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. + +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Pyro.Domain.Shared.Email; + +namespace Pyro.Domain.Identity.Commands; + +public record NotifyExpiringPasswords : IRequest; + +public class NotifyExpiringPasswordsHandler : IRequestHandler +{ + private readonly ILogger logger; + private readonly IUserRepository repository; + private readonly IEmailService emailService; + private readonly EmailServiceOptions emailServiceOptions; + + public NotifyExpiringPasswordsHandler( + ILogger logger, + IUserRepository repository, + IEmailService emailService, + IOptions emailServiceOptions) + { + this.logger = logger; + this.repository = repository; + this.emailService = emailService; + this.emailServiceOptions = emailServiceOptions.Value; + } + + public async Task Handle(NotifyExpiringPasswords request, CancellationToken cancellationToken = default) + { + var count = 0; + var expiringUsers = repository.GetUsersWithExpiringPasswords().WithCancellation(cancellationToken); + await foreach (var user in expiringUsers) + { + var body = $""" + Hello {user.Profile.Name}, + + Your password is about to expire. The expiration date is {user.PasswordExpiresAt:yyyy-MM-dd}. + Please change your password to avoid any issues. + + Thank you, Pyro. + """; + var email = new EmailMessage( + new EmailAddress(user.Profile.Name, user.Login), + new EmailAddress("No Reply", $"no-reply@{emailServiceOptions.Domain}"), + "Password expiration", + body); + + await emailService.SendEmail(email, cancellationToken); + + logger.LogDebug("Notified '{User}' about expiring password", user.Login); + count++; + } + + if (count > 0) + logger.LogInformation("Notified {Count} users about expiring passwords", count); + } +} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity/Commands/RefreshToken.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/RefreshToken.cs index ca817a6d..ff02d48e 100644 --- a/Pyro.Api/Pyro.Domain.Identity/Commands/RefreshToken.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/RefreshToken.cs @@ -64,6 +64,13 @@ public async Task Handle(RefreshToken request, CancellationT return RefreshTokenResult.Fail(); } + if (user.PasswordExpiresAt < timeProvider.GetUtcNow()) + { + logger.LogWarning("The password of '{User}' user is expired", user.Login); + + return RefreshTokenResult.Fail(); + } + var token = user.GetAuthenticationToken(jwtToken.TokenId); if (token is null) { diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/UpdateProfile.cs b/Pyro.Api/Pyro.Domain.Identity/Commands/UpdateProfile.cs similarity index 73% rename from Pyro.Api/Pyro.Domain/UserProfiles/UpdateProfile.cs rename to Pyro.Api/Pyro.Domain.Identity/Commands/UpdateProfile.cs index b7134626..4a56868d 100644 --- a/Pyro.Api/Pyro.Domain/UserProfiles/UpdateProfile.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Commands/UpdateProfile.cs @@ -6,7 +6,7 @@ using Pyro.Domain.Shared.CurrentUserProvider; using Pyro.Domain.Shared.Exceptions; -namespace Pyro.Domain.UserProfiles; +namespace Pyro.Domain.Identity.Commands; public record UpdateProfile(string Name, string? Status) : IRequest; @@ -26,11 +26,11 @@ public UpdateProfileValidator() public class UpdateProfileHandler : IRequestHandler { private readonly ICurrentUserProvider currentUserProvider; - private readonly IUserProfileRepository repository; + private readonly IUserRepository repository; public UpdateProfileHandler( ICurrentUserProvider currentUserProvider, - IUserProfileRepository repository) + IUserRepository repository) { this.currentUserProvider = currentUserProvider; this.repository = repository; @@ -39,10 +39,10 @@ public UpdateProfileHandler( public async Task Handle(UpdateProfile request, CancellationToken cancellationToken) { var currentUser = currentUserProvider.GetCurrentUser(); - var profile = await repository.GetUserProfile(currentUser.Id, cancellationToken) ?? - throw new NotFoundException("User profile not found"); + var user = await repository.GetUserById(currentUser.Id, cancellationToken) ?? + throw new NotFoundException($"User with id {currentUser.Id} is not found"); - profile.Name = request.Name; - profile.Status = request.Status; + user.Profile.Name = request.Name; + user.Profile.Status = request.Status; } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity/IUserRepository.cs b/Pyro.Api/Pyro.Domain.Identity/IUserRepository.cs index 8630850c..0d9fae84 100644 --- a/Pyro.Api/Pyro.Domain.Identity/IUserRepository.cs +++ b/Pyro.Api/Pyro.Domain.Identity/IUserRepository.cs @@ -29,4 +29,6 @@ Task> GetUsers( Task> GetRolesAsync(CancellationToken cancellationToken = default); Task> GetPermissionsAsync(CancellationToken cancellationToken = default); + + IAsyncEnumerable GetUsersWithExpiringPasswords(); } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain.Identity/Models/User.cs b/Pyro.Api/Pyro.Domain.Identity/Models/User.cs index 958b9b8f..e0914585 100644 --- a/Pyro.Api/Pyro.Domain.Identity/Models/User.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Models/User.cs @@ -18,6 +18,7 @@ public class User : DomainEntity private byte[] password = []; private byte[] salt = []; + private DateTimeOffset passwordExpiresAt; public static User Create(string login, byte[] password, byte[] salt) { @@ -26,7 +27,9 @@ public static User Create(string login, byte[] password, byte[] salt) Login = login, Password = password, Salt = salt, + PasswordExpiresAt = DateTimeOffset.MinValue, IsLocked = true, + Profile = new UserProfile { Name = login }, }; user.PublishEvent(new UserCreated(user.Id, login)); @@ -69,6 +72,12 @@ public IReadOnlyList Salt } } + public DateTimeOffset PasswordExpiresAt + { + get => passwordExpiresAt; + init => passwordExpiresAt = value; + } + public bool IsLocked { get; private set; } public IReadOnlyList Roles @@ -83,6 +92,8 @@ public IReadOnlyList AccessTokens public IReadOnlyList OneTimePasswords => oneTimePasswords; + public required UserProfile Profile { get; init; } + public void Lock() { IsLocked = true; @@ -143,14 +154,24 @@ public void DeleteAccessToken(string name) accessTokens.Remove(accessToken); } - public void ChangePassword(IPasswordService passwordService, string oldPassword, string newPassword) + private void ChangePassword(TimeProvider timeProvider, IPasswordService passwordService, string newPassword) { - if (!passwordService.VerifyPassword(oldPassword, password, salt)) - throw new DomainException("The old password is incorrect."); - var (newPasswordHash, newSalt) = passwordService.GeneratePasswordHash(newPassword); Password = newPasswordHash; Salt = newSalt; + passwordExpiresAt = timeProvider.GetUtcNow().AddDays(90); // TODO: config + } + + public void ChangePassword( + TimeProvider timeProvider, + IPasswordService passwordService, + string oldPassword, + string newPassword) + { + if (!passwordService.VerifyPassword(oldPassword, password, salt)) + throw new DomainException("The old password is incorrect."); + + ChangePassword(timeProvider, passwordService, newPassword); } public void AddOneTimePassword(OneTimePassword oneTimePassword) @@ -172,9 +193,9 @@ private void DeleteOneTimePassword(OneTimePassword oneTimePassword) public void Activate( TimeProvider timeProvider, + IPasswordService passwordService, OneTimePassword oneTimePassword, - byte[] newPassword, - byte[] newSalt) + string newPassword) { if (oneTimePassword is null) throw new ArgumentNullException(nameof(oneTimePassword)); @@ -185,8 +206,7 @@ public void Activate( if (!IsLocked) throw new DomainException($"The user '{Login}' is already activated."); - password = newPassword; - salt = newSalt; + ChangePassword(timeProvider, passwordService, newPassword); Unlock(); DeleteOneTimePassword(oneTimePassword); } diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/UserProfile.cs b/Pyro.Api/Pyro.Domain.Identity/Models/UserProfile.cs similarity index 59% rename from Pyro.Api/Pyro.Domain/UserProfiles/UserProfile.cs rename to Pyro.Api/Pyro.Domain.Identity/Models/UserProfile.cs index 01686cee..6f3fabbb 100644 --- a/Pyro.Api/Pyro.Domain/UserProfiles/UserProfile.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Models/UserProfile.cs @@ -3,17 +3,11 @@ using Pyro.Domain.Shared.Entities; -namespace Pyro.Domain.UserProfiles; +namespace Pyro.Domain.Identity.Models; public class UserProfile : Entity { - public static readonly Guid Pyro = Guid.Parse("F9BA057A-35B0-4D10-8326-702D8F7EC966"); - public required string Name { get; set; } public string? Status { get; set; } - - public UserAvatar? Avatar { get; init; } - - public User User { get; init; } = null!; } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/GetUserProfile.cs b/Pyro.Api/Pyro.Domain.Identity/Queries/GetUserProfile.cs similarity index 69% rename from Pyro.Api/Pyro.Domain/UserProfiles/GetUserProfile.cs rename to Pyro.Api/Pyro.Domain.Identity/Queries/GetUserProfile.cs index 6d21b29f..537b4dde 100644 --- a/Pyro.Api/Pyro.Domain/UserProfiles/GetUserProfile.cs +++ b/Pyro.Api/Pyro.Domain.Identity/Queries/GetUserProfile.cs @@ -2,21 +2,22 @@ // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. using MediatR; +using Pyro.Domain.Identity.Models; using Pyro.Domain.Shared.CurrentUserProvider; using Pyro.Domain.Shared.Exceptions; -namespace Pyro.Domain.UserProfiles; +namespace Pyro.Domain.Identity.Queries; public record GetUserProfile : IRequest; public class GetUserProfileHandler : IRequestHandler { private readonly ICurrentUserProvider currentUserProvider; - private readonly IUserProfileRepository repository; + private readonly IUserRepository repository; public GetUserProfileHandler( ICurrentUserProvider currentUserProvider, - IUserProfileRepository repository) + IUserRepository repository) { this.currentUserProvider = currentUserProvider; this.repository = repository; @@ -25,10 +26,9 @@ public GetUserProfileHandler( public async Task Handle(GetUserProfile request, CancellationToken cancellationToken) { var currentUser = currentUserProvider.GetCurrentUser(); - var profile = await repository.GetUserProfile(currentUser.Id, cancellationToken); - if (profile is null) - throw new NotFoundException("User profile not found"); + var user = await repository.GetUserById(currentUser.Id, cancellationToken) ?? + throw new NotFoundException($"User with id {currentUser.Id} not found"); - return profile; + return user.Profile; } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Shared/Email/EmailServiceOptions.cs b/Pyro.Api/Pyro.Domain.Shared/Email/EmailServiceOptions.cs similarity index 97% rename from Pyro.Api/Pyro.Infrastructure.Shared/Email/EmailServiceOptions.cs rename to Pyro.Api/Pyro.Domain.Shared/Email/EmailServiceOptions.cs index cdbf0b56..1454dc39 100644 --- a/Pyro.Api/Pyro.Infrastructure.Shared/Email/EmailServiceOptions.cs +++ b/Pyro.Api/Pyro.Domain.Shared/Email/EmailServiceOptions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; -namespace Pyro.Infrastructure.Shared.Email; +namespace Pyro.Domain.Shared.Email; public class EmailServiceOptions : IValidateOptions { diff --git a/Pyro.Api/Pyro.Domain.Shared/Pyro.Domain.Shared.csproj b/Pyro.Api/Pyro.Domain.Shared/Pyro.Domain.Shared.csproj index 56adc5ea..b9c8ea93 100644 --- a/Pyro.Api/Pyro.Domain.Shared/Pyro.Domain.Shared.csproj +++ b/Pyro.Api/Pyro.Domain.Shared/Pyro.Domain.Shared.csproj @@ -9,6 +9,7 @@ + diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/CreateUserProfile.cs b/Pyro.Api/Pyro.Domain/UserProfiles/CreateUserProfile.cs deleted file mode 100644 index dd84a36e..00000000 --- a/Pyro.Api/Pyro.Domain/UserProfiles/CreateUserProfile.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using FluentValidation; -using MediatR; - -namespace Pyro.Domain.UserProfiles; - -public record CreateUserProfile(Guid UserId, string Name) : IRequest; - -public class CreateUserProfileValidator : AbstractValidator -{ - public CreateUserProfileValidator() - { - RuleFor(x => x.UserId) - .NotEmpty(); - - RuleFor(x => x.Name) - .NotEmpty() - .MaximumLength(50); - } -} - -public class CreateUserProfileHandler : IRequestHandler -{ - private readonly IUserProfileRepository repository; - - public CreateUserProfileHandler(IUserProfileRepository repository) - => this.repository = repository; - - public async Task Handle(CreateUserProfile request, CancellationToken cancellationToken = default) - { - var userProfile = new UserProfile - { - Id = request.UserId, - Name = request.Name, - }; - await repository.AddUserProfile(userProfile, cancellationToken); - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/IUserProfileRepository.cs b/Pyro.Api/Pyro.Domain/UserProfiles/IUserProfileRepository.cs deleted file mode 100644 index 9edf4e78..00000000 --- a/Pyro.Api/Pyro.Domain/UserProfiles/IUserProfileRepository.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -namespace Pyro.Domain.UserProfiles; - -public interface IUserProfileRepository -{ - Task GetUserProfile(Guid userId, CancellationToken cancellationToken); - - Task AddUserProfile(UserProfile userProfile, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/User.cs b/Pyro.Api/Pyro.Domain/UserProfiles/User.cs deleted file mode 100644 index 7e3ad394..00000000 --- a/Pyro.Api/Pyro.Domain/UserProfiles/User.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -namespace Pyro.Domain.UserProfiles; - -public record User(Guid Id, string Email); \ No newline at end of file diff --git a/Pyro.Api/Pyro.Domain/UserProfiles/UserAvatar.cs b/Pyro.Api/Pyro.Domain/UserProfiles/UserAvatar.cs deleted file mode 100644 index 5a989d0e..00000000 --- a/Pyro.Api/Pyro.Domain/UserProfiles/UserAvatar.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using Pyro.Domain.Shared.Entities; - -namespace Pyro.Domain.UserProfiles; - -public class UserAvatar : Entity -{ -#pragma warning disable CA1819 - public required byte[] Image { get; init; } -#pragma warning restore CA1819 -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/SeedData.cs b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/SeedData.cs index 4a61b575..79765b63 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/SeedData.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/SeedData.cs @@ -12,6 +12,7 @@ public static class SeedData public static readonly IReadOnlyCollection Permissions; public static readonly IReadOnlyCollection Roles; public static readonly IReadOnlyCollection Users; + public static readonly IReadOnlyCollection UserProfiles; public static readonly IReadOnlyCollection RolePermissions; public static readonly IReadOnlyCollection UserRoles; @@ -46,6 +47,17 @@ static SeedData() Login = "pyro@localhost.local", Password = [239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197], Salt = [109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132], + PasswordExpiresAt = new DateTimeOffset(2100, 01, 01, 0, 0, 0, TimeSpan.Zero), + Profile = null!, + } + ]; + + UserProfiles = + [ + new UserProfile + { + Id = User.PyroUser, + Name = "Pyro", } ]; diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserConfiguration.cs b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserConfiguration.cs index 6f147e5b..ffee0692 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserConfiguration.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserConfiguration.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Pyro.Domain.Identity.Models; namespace Pyro.Infrastructure.Identity.DataAccess.Configurations; @@ -39,6 +40,11 @@ public void Configure(EntityTypeBuilder builder) .UsePropertyAccessMode(PropertyAccessMode.Field); builder.Ignore(x => x.Salt); + builder.Property(x => x.PasswordExpiresAt) + .IsRequired() + .UsePropertyAccessMode(PropertyAccessMode.Field) + .HasConversion(); + builder.Property(x => x.IsLocked) .HasDefaultValue(false); @@ -81,6 +87,11 @@ public void Configure(EntityTypeBuilder builder) .WithOne() .OnDelete(DeleteBehavior.Cascade); + builder.HasOne(x => x.Profile) + .WithOne() + .HasForeignKey(x => x.Id) + .HasPrincipalKey(x => x.Id); + builder.HasIndex(x => x.Login) .IsUnique() .HasDatabaseName("IX_Users_Login"); diff --git a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserProfileConfiguration.cs b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserProfileConfiguration.cs similarity index 68% rename from Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserProfileConfiguration.cs rename to Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserProfileConfiguration.cs index 8e5711e5..3d4bfa18 100644 --- a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserProfileConfiguration.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/Configurations/UserProfileConfiguration.cs @@ -3,9 +3,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Pyro.Domain.UserProfiles; +using Pyro.Domain.Identity.Models; -namespace Pyro.Infrastructure.DataAccess.Configurations.UserProfiles; +namespace Pyro.Infrastructure.Identity.DataAccess.Configurations; internal class UserProfileConfiguration : IEntityTypeConfiguration { @@ -26,15 +26,6 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Status) .HasMaxLength(200); - builder.HasOne(x => x.Avatar) - .WithOne() - .HasForeignKey(x => x.Id); - - builder.HasOne(x => x.User) - .WithOne() - .HasForeignKey(x => x.Id) - .HasPrincipalKey(x => x.Id); - builder.HasData(SeedData.UserProfiles); } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/UserRepository.cs b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/UserRepository.cs index 266c5213..e0dfcd7e 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/UserRepository.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/DataAccess/UserRepository.cs @@ -11,9 +11,13 @@ namespace Pyro.Infrastructure.Identity.DataAccess; internal class UserRepository : IUserRepository { private readonly IdentityDbContext dbContext; + private readonly TimeProvider timeProvider; - public UserRepository(IdentityDbContext dbContext) - => this.dbContext = dbContext; + public UserRepository(IdentityDbContext dbContext, TimeProvider timeProvider) + { + this.dbContext = dbContext; + this.timeProvider = timeProvider; + } public async Task> GetUsers( GetUsers query, @@ -105,6 +109,16 @@ public async Task> GetPermissionsAsync(CancellationTok return permissions; } + public IAsyncEnumerable GetUsersWithExpiringPasswords() + { + var now = timeProvider.GetUtcNow(); + var expiration = now.AddDays(7); // TODO: config + + return Users + .Where(x => !x.IsLocked && x.PasswordExpiresAt < expiration) + .AsAsyncEnumerable(); + } + private IQueryable Users => dbContext .Set() @@ -113,5 +127,6 @@ private IQueryable Users .Include(x => x.AuthenticationTokens) .Include(x => x.AccessTokens) .Include(x => x.OneTimePasswords) + .Include(x => x.Profile) .AsSplitQuery(); } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.Designer.cs deleted file mode 100644 index b131efbb..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.Designer.cs +++ /dev/null @@ -1,419 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Identity.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Identity.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20240812212140_Initial")] - partial class Initial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("salt") - .IsRequired() - .HasMaxLength(16) - .HasColumnType("BLOB") - .HasColumnName("Salt"); - - b.Property("token") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("BLOB") - .HasColumnName("Token"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_AccessTokens_Name"); - - b.HasIndex("UserId"); - - b.ToTable("AccessTokens", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AuthenticationToken", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("TokenId") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("TokenId") - .IsUnique() - .HasDatabaseName("IX_AuthenticationTokens_TokenId"); - - b.HasIndex("UserId"); - - b.ToTable("AuthenticationTokens", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.Permission", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Permissions_Name"); - - b.ToTable("Permissions", (string)null); - - b.HasData( - new - { - Id = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b"), - Name = "repository.view" - }, - new - { - Id = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f"), - Name = "repository.edit" - }, - new - { - Id = new Guid("a740c470-34ea-46c4-8ca0-dc692e1fb423"), - Name = "repository.manage" - }, - new - { - Id = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405"), - Name = "user.view" - }, - new - { - Id = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70"), - Name = "user.edit" - }, - new - { - Id = new Guid("e6a86676-0f74-4d00-a9b7-fc84a065d673"), - Name = "user.manage" - }, - new - { - Id = new Guid("6a351a07-b36e-417e-8cac-33f86f413011"), - Name = "issue.view" - }, - new - { - Id = new Guid("773663af-5e24-4468-98c2-607957469e8c"), - Name = "issue.edit" - }, - new - { - Id = new Guid("327c3c6f-eef4-4f02-b865-cab4d1e550f9"), - Name = "issue.manage" - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.Role", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Roles_Name"); - - b.ToTable("Roles", (string)null); - - b.HasData( - new - { - Id = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - Name = "Admin" - }, - new - { - Id = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - Name = "User" - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.SigningKey", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SigningKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.User", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("IsLocked") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Login") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("password") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("BLOB") - .HasColumnName("Password"); - - b.Property("salt") - .IsRequired() - .HasMaxLength(16) - .HasColumnType("BLOB") - .HasColumnName("Salt"); - - b.HasKey("Id"); - - b.HasIndex("Login") - .IsUnique() - .HasDatabaseName("IX_Users_Login"); - - b.ToTable("Users", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - IsLocked = false, - Login = "pyro", - password = new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, - salt = new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } - }); - }); - - modelBuilder.Entity("RolePermission", b => - { - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.Property("PermissionId") - .HasColumnType("TEXT"); - - b.HasKey("RoleId", "PermissionId"); - - b.HasIndex("PermissionId"); - - b.ToTable("RolePermissions", (string)null); - - b.HasData( - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("a740c470-34ea-46c4-8ca0-dc692e1fb423") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("e6a86676-0f74-4d00-a9b7-fc84a065d673") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("6a351a07-b36e-417e-8cac-33f86f413011") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("773663af-5e24-4468-98c2-607957469e8c") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("327c3c6f-eef4-4f02-b865-cab4d1e550f9") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("6a351a07-b36e-417e-8cac-33f86f413011") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("773663af-5e24-4468-98c2-607957469e8c") - }); - }); - - modelBuilder.Entity("UserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRoles", (string)null); - - b.HasData( - new - { - UserId = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d") - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => - { - b.HasOne("Pyro.Domain.Identity.Models.User", null) - .WithMany("AccessTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AuthenticationToken", b => - { - b.HasOne("Pyro.Domain.Identity.Models.User", "User") - .WithMany("AuthenticationTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RolePermission", b => - { - b.HasOne("Pyro.Domain.Identity.Models.Permission", null) - .WithMany() - .HasForeignKey("PermissionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Identity.Models.Role", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("UserRole", b => - { - b.HasOne("Pyro.Domain.Identity.Models.Role", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Identity.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.User", b => - { - b.Navigation("AccessTokens"); - - b.Navigation("AuthenticationTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.Designer.cs deleted file mode 100644 index 032acd2f..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.Designer.cs +++ /dev/null @@ -1,419 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Identity.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Identity.Migrations -{ - [DbContext(typeof(IdentityDbContext))] - [Migration("20241028213426_UpdatePyroLogin")] - partial class UpdatePyroLogin - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("salt") - .IsRequired() - .HasMaxLength(16) - .HasColumnType("BLOB") - .HasColumnName("Salt"); - - b.Property("token") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("BLOB") - .HasColumnName("Token"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_AccessTokens_Name"); - - b.HasIndex("UserId"); - - b.ToTable("AccessTokens", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AuthenticationToken", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ExpiresAt") - .HasColumnType("TEXT"); - - b.Property("TokenId") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("TokenId") - .IsUnique() - .HasDatabaseName("IX_AuthenticationTokens_TokenId"); - - b.HasIndex("UserId"); - - b.ToTable("AuthenticationTokens", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.Permission", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Permissions_Name"); - - b.ToTable("Permissions", (string)null); - - b.HasData( - new - { - Id = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b"), - Name = "repository.view" - }, - new - { - Id = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f"), - Name = "repository.edit" - }, - new - { - Id = new Guid("a740c470-34ea-46c4-8ca0-dc692e1fb423"), - Name = "repository.manage" - }, - new - { - Id = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405"), - Name = "user.view" - }, - new - { - Id = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70"), - Name = "user.edit" - }, - new - { - Id = new Guid("e6a86676-0f74-4d00-a9b7-fc84a065d673"), - Name = "user.manage" - }, - new - { - Id = new Guid("6a351a07-b36e-417e-8cac-33f86f413011"), - Name = "issue.view" - }, - new - { - Id = new Guid("773663af-5e24-4468-98c2-607957469e8c"), - Name = "issue.edit" - }, - new - { - Id = new Guid("327c3c6f-eef4-4f02-b865-cab4d1e550f9"), - Name = "issue.manage" - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.Role", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Roles_Name"); - - b.ToTable("Roles", (string)null); - - b.HasData( - new - { - Id = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - Name = "Admin" - }, - new - { - Id = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - Name = "User" - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.SigningKey", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("SigningKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.User", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("IsLocked") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Login") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("password") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("BLOB") - .HasColumnName("Password"); - - b.Property("salt") - .IsRequired() - .HasMaxLength(16) - .HasColumnType("BLOB") - .HasColumnName("Salt"); - - b.HasKey("Id"); - - b.HasIndex("Login") - .IsUnique() - .HasDatabaseName("IX_Users_Login"); - - b.ToTable("Users", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - IsLocked = false, - Login = "pyro@localhost.local", - password = new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, - salt = new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } - }); - }); - - modelBuilder.Entity("RolePermission", b => - { - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.Property("PermissionId") - .HasColumnType("TEXT"); - - b.HasKey("RoleId", "PermissionId"); - - b.HasIndex("PermissionId"); - - b.ToTable("RolePermissions", (string)null); - - b.HasData( - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("a740c470-34ea-46c4-8ca0-dc692e1fb423") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("e6a86676-0f74-4d00-a9b7-fc84a065d673") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("6a351a07-b36e-417e-8cac-33f86f413011") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("773663af-5e24-4468-98c2-607957469e8c") - }, - new - { - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d"), - PermissionId = new Guid("327c3c6f-eef4-4f02-b865-cab4d1e550f9") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("f65ad9fd-a259-4598-803a-f85607c7566b") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("edf38b44-b150-46df-bc79-adaa3c01659f") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("95fed72d-90b3-4104-891e-a7dae7ea4405") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("2c182139-085d-4851-aa3b-ca218ee77e70") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("6a351a07-b36e-417e-8cac-33f86f413011") - }, - new - { - RoleId = new Guid("36b9e20e-9b6b-461b-b129-d6a49fe4f4f8"), - PermissionId = new Guid("773663af-5e24-4468-98c2-607957469e8c") - }); - }); - - modelBuilder.Entity("UserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("UserRoles", (string)null); - - b.HasData( - new - { - UserId = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - RoleId = new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d") - }); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => - { - b.HasOne("Pyro.Domain.Identity.Models.User", null) - .WithMany("AccessTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.AuthenticationToken", b => - { - b.HasOne("Pyro.Domain.Identity.Models.User", "User") - .WithMany("AuthenticationTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("RolePermission", b => - { - b.HasOne("Pyro.Domain.Identity.Models.Permission", null) - .WithMany() - .HasForeignKey("PermissionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Identity.Models.Role", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("UserRole", b => - { - b.HasOne("Pyro.Domain.Identity.Models.Role", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Identity.Models.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.Identity.Models.User", b => - { - b.Navigation("AccessTokens"); - - b.Navigation("AuthenticationTokens"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.cs deleted file mode 100644 index fb02441b..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241028213426_UpdatePyroLogin.cs +++ /dev/null @@ -1,34 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Identity.Migrations -{ - /// - public partial class UpdatePyroLogin : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.UpdateData( - table: "Users", - keyColumn: "Id", - keyValue: new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - column: "Login", - value: "pyro@localhost.local"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.UpdateData( - table: "Users", - keyColumn: "Id", - keyValue: new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - column: "Login", - value: "pyro"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.cs deleted file mode 100644 index d2ec6b82..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.cs +++ /dev/null @@ -1,69 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Identity.Migrations -{ - /// - public partial class AddOneTimePasswords : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "OneTimePasswords", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Token = table.Column(type: "TEXT", maxLength: 32, nullable: false), - ExpiresAt = table.Column(type: "INTEGER", nullable: false), - Purpose = table.Column(type: "INTEGER", nullable: false), - UserId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OneTimePasswords", x => x.Id); - table.ForeignKey( - name: "FK_OneTimePasswords_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.UpdateData( - table: "Users", - keyColumn: "Id", - keyValue: new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - column: "Login", - value: "pyro@localhost.local"); - - migrationBuilder.CreateIndex( - name: "IX_OneTimePasswords_Token", - table: "OneTimePasswords", - column: "Token", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_OneTimePasswords_UserId", - table: "OneTimePasswords", - column: "UserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "OneTimePasswords"); - - migrationBuilder.UpdateData( - table: "Users", - keyColumn: "Id", - keyValue: new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - column: "Login", - value: "pyro"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.Designer.cs similarity index 91% rename from Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.Designer.cs rename to Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.Designer.cs index 2365fd77..5d1ebbea 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241106114633_AddOneTimePasswords.Designer.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.Designer.cs @@ -11,14 +11,14 @@ namespace Pyro.Infrastructure.Identity.Migrations { [DbContext(typeof(IdentityDbContext))] - [Migration("20241106114633_AddOneTimePasswords")] - partial class AddOneTimePasswords + [Migration("20241118140123_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => { @@ -242,6 +242,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(32) .HasColumnType("TEXT"); + b.Property("PasswordExpiresAt") + .HasColumnType("INTEGER"); + b.Property("password") .IsRequired() .HasMaxLength(64) @@ -268,11 +271,39 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), IsLocked = false, Login = "pyro@localhost.local", + PasswordExpiresAt = 1356555091968000000L, password = new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, salt = new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } }); }); + modelBuilder.Entity("Pyro.Domain.Identity.Models.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_UserProfile"); + + b.ToTable("UserProfiles", (string)null); + + b.HasData( + new + { + Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), + Name = "Pyro" + }); + }); + modelBuilder.Entity("RolePermission", b => { b.Property("RoleId") @@ -419,6 +450,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Pyro.Domain.Identity.Models.UserProfile", b => + { + b.HasOne("Pyro.Domain.Identity.Models.User", null) + .WithOne("Profile") + .HasForeignKey("Pyro.Domain.Identity.Models.UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("RolePermission", b => { b.HasOne("Pyro.Domain.Identity.Models.Permission", null) @@ -456,6 +496,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("AuthenticationTokens"); b.Navigation("OneTimePasswords"); + + b.Navigation("Profile") + .IsRequired(); }); #pragma warning restore 612, 618 } diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.cs similarity index 79% rename from Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.cs rename to Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.cs index a809d496..2073662c 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20240812212140_Initial.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/20241118140123_Initial.cs @@ -57,6 +57,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "TEXT", nullable: false), Login = table.Column(type: "TEXT", maxLength: 32, nullable: false), + PasswordExpiresAt = table.Column(type: "INTEGER", nullable: false), IsLocked = table.Column(type: "INTEGER", nullable: false, defaultValue: false), Password = table.Column(type: "BLOB", maxLength: 64, nullable: false), Salt = table.Column(type: "BLOB", maxLength: 16, nullable: false) @@ -132,6 +133,46 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "OneTimePasswords", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Token = table.Column(type: "TEXT", maxLength: 32, nullable: false), + ExpiresAt = table.Column(type: "INTEGER", nullable: false), + Purpose = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OneTimePasswords", x => x.Id); + table.ForeignKey( + name: "FK_OneTimePasswords_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserProfiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Status = table.Column(type: "TEXT", maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserProfile", x => x.Id); + table.ForeignKey( + name: "FK_UserProfiles_Users_Id", + column: x => x.Id, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "UserRoles", columns: table => new @@ -183,8 +224,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.InsertData( table: "Users", - columns: new[] { "Id", "Login", "Password", "Salt" }, - values: new object[] { new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), "pyro", new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } }); + columns: new[] { "Id", "Login", "PasswordExpiresAt", "Password", "Salt" }, + values: new object[] { new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), "pyro@localhost.local", 1356555091968000000L, new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } }); migrationBuilder.InsertData( table: "RolePermissions", @@ -208,6 +249,11 @@ protected override void Up(MigrationBuilder migrationBuilder) { new Guid("f65ad9fd-a259-4598-803a-f85607c7566b"), new Guid("9aa993eb-e3db-4fce-ba9f-b0bb23395b9d") } }); + migrationBuilder.InsertData( + table: "UserProfiles", + columns: new[] { "Id", "Name", "Status" }, + values: new object[] { new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), "Pyro", null }); + migrationBuilder.InsertData( table: "UserRoles", columns: new[] { "RoleId", "UserId" }, @@ -235,6 +281,17 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "AuthenticationTokens", column: "UserId"); + migrationBuilder.CreateIndex( + name: "IX_OneTimePasswords_Token", + table: "OneTimePasswords", + column: "Token", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OneTimePasswords_UserId", + table: "OneTimePasswords", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_Permissions_Name", table: "Permissions", @@ -273,12 +330,18 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "AuthenticationTokens"); + migrationBuilder.DropTable( + name: "OneTimePasswords"); + migrationBuilder.DropTable( name: "RolePermissions"); migrationBuilder.DropTable( name: "SigningKeys"); + migrationBuilder.DropTable( + name: "UserProfiles"); + migrationBuilder.DropTable( name: "UserRoles"); diff --git a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/IdentityDbContextModelSnapshot.cs b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/IdentityDbContextModelSnapshot.cs index d945bff2..2b3a569c 100644 --- a/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/IdentityDbContextModelSnapshot.cs +++ b/Pyro.Api/Pyro.Infrastructure.Identity/Migrations/IdentityDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class IdentityDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Pyro.Domain.Identity.Models.AccessToken", b => { @@ -239,6 +239,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(32) .HasColumnType("TEXT"); + b.Property("PasswordExpiresAt") + .HasColumnType("INTEGER"); + b.Property("password") .IsRequired() .HasMaxLength(64) @@ -265,11 +268,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), IsLocked = false, Login = "pyro@localhost.local", + PasswordExpiresAt = 1356555091968000000L, password = new byte[] { 239, 163, 54, 78, 41, 129, 181, 60, 27, 181, 100, 116, 243, 128, 253, 209, 87, 147, 27, 73, 138, 190, 50, 65, 18, 253, 153, 127, 194, 97, 240, 29, 179, 58, 68, 117, 170, 97, 172, 236, 70, 27, 167, 168, 87, 3, 66, 53, 11, 34, 206, 209, 211, 150, 81, 227, 19, 161, 249, 24, 45, 138, 206, 197 }, salt = new byte[] { 109, 28, 230, 18, 208, 250, 67, 218, 171, 6, 152, 200, 162, 109, 186, 132 } }); }); + modelBuilder.Entity("Pyro.Domain.Identity.Models.UserProfile", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_UserProfile"); + + b.ToTable("UserProfiles", (string)null); + + b.HasData( + new + { + Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), + Name = "Pyro" + }); + }); + modelBuilder.Entity("RolePermission", b => { b.Property("RoleId") @@ -416,6 +447,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Pyro.Domain.Identity.Models.UserProfile", b => + { + b.HasOne("Pyro.Domain.Identity.Models.User", null) + .WithOne("Profile") + .HasForeignKey("Pyro.Domain.Identity.Models.UserProfile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("RolePermission", b => { b.HasOne("Pyro.Domain.Identity.Models.Permission", null) @@ -453,6 +493,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AuthenticationTokens"); b.Navigation("OneTimePasswords"); + + b.Navigation("Profile") + .IsRequired(); }); #pragma warning restore 612, 618 } diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.Designer.cs deleted file mode 100644 index 177afa58..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.Designer.cs +++ /dev/null @@ -1,214 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240812212127_Initial")] - partial class Initial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issue", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("Comments"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.cs deleted file mode 100644 index 07e34a9f..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240812212127_Initial.cs +++ /dev/null @@ -1,132 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Issue", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - IssueNumber = table.Column(type: "INTEGER", nullable: false), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - RepositoryId = table.Column(type: "TEXT", nullable: false), - AuthorId = table.Column(type: "TEXT", nullable: false), - CreatedAt = table.Column(type: "INTEGER", nullable: false), - AssigneeId = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Issue", x => x.Id); - table.ForeignKey( - name: "FK_Issue_GitRepositories_RepositoryId", - column: x => x.RepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Issue_UserProfiles_AssigneeId", - column: x => x.AssigneeId, - principalTable: "UserProfiles", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_Issue_UserProfiles_AuthorId", - column: x => x.AuthorId, - principalTable: "UserProfiles", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "IssueNumberTracker", - columns: table => new - { - RepositoryId = table.Column(type: "TEXT", nullable: false), - Number = table.Column(type: "INTEGER", nullable: false, defaultValue: 0) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueNumberTracker", x => x.RepositoryId); - table.ForeignKey( - name: "FK_IssueNumberTracker_Repository", - column: x => x.RepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "IssueComments", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Content = table.Column(type: "TEXT", maxLength: 2000, nullable: false), - IssueId = table.Column(type: "TEXT", nullable: false), - AuthorId = table.Column(type: "TEXT", nullable: false), - CreatedAt = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueComment", x => x.Id); - table.ForeignKey( - name: "FK_IssueComments_Issue_IssueId", - column: x => x.IssueId, - principalTable: "Issue", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_IssueComments_UserProfiles_AuthorId", - column: x => x.AuthorId, - principalTable: "UserProfiles", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Issue_AssigneeId", - table: "Issue", - column: "AssigneeId"); - - migrationBuilder.CreateIndex( - name: "IX_Issue_AuthorId", - table: "Issue", - column: "AuthorId"); - - migrationBuilder.CreateIndex( - name: "IX_Issue_RepositoryId_Number", - table: "Issue", - columns: new[] { "RepositoryId", "IssueNumber" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_IssueComments_AuthorId", - table: "IssueComments", - column: "AuthorId"); - - migrationBuilder.CreateIndex( - name: "IX_IssueComments_IssueId", - table: "IssueComments", - column: "IssueId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "IssueComments"); - - migrationBuilder.DropTable( - name: "IssueNumberTracker"); - - migrationBuilder.DropTable( - name: "Issue"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.Designer.cs deleted file mode 100644 index f581aff1..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.Designer.cs +++ /dev/null @@ -1,288 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240818120513_AddTags")] - partial class AddTags - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("IssueTag", b => - { - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.Property("TagId") - .HasColumnType("TEXT"); - - b.HasKey("IssueId", "TagId"); - - b.HasIndex("TagId"); - - b.ToTable("IssueTags", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issues", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Tag", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Tag"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Tag_Name"); - - b.ToTable("Tags", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("IssueTag", b => - { - b.HasOne("Pyro.Domain.Issues.Issue", null) - .WithMany() - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Tag", null) - .WithMany() - .HasForeignKey("TagId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Tag", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany("Tags") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Navigation("Tags"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("Comments"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.cs deleted file mode 100644 index 22f4bb7f..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240818120513_AddTags.cs +++ /dev/null @@ -1,170 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class AddTags : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Issue_GitRepositories_RepositoryId", - table: "Issue"); - - migrationBuilder.DropForeignKey( - name: "FK_Issue_UserProfiles_AssigneeId", - table: "Issue"); - - migrationBuilder.DropForeignKey( - name: "FK_Issue_UserProfiles_AuthorId", - table: "Issue"); - - migrationBuilder.DropForeignKey( - name: "FK_IssueComments_Issue_IssueId", - table: "IssueComments"); - - migrationBuilder.RenameTable( - name: "Issue", - newName: "Issues"); - - migrationBuilder.RenameIndex( - name: "IX_Issue_AuthorId", - table: "Issues", - newName: "IX_Issues_AuthorId"); - - migrationBuilder.RenameIndex( - name: "IX_Issue_AssigneeId", - table: "Issues", - newName: "IX_Issues_AssigneeId"); - - migrationBuilder.CreateTable( - name: "IssueTags", - columns: table => new - { - IssueId = table.Column(type: "TEXT", nullable: false), - TagId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueTags", x => new { x.IssueId, x.TagId }); - table.ForeignKey( - name: "FK_IssueTags_Issues_IssueId", - column: x => x.IssueId, - principalTable: "Issues", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_IssueTags_Tags_TagId", - column: x => x.TagId, - principalTable: "Tags", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_IssueTags_TagId", - table: "IssueTags", - column: "TagId"); - - migrationBuilder.AddForeignKey( - name: "FK_IssueComments_Issues_IssueId", - table: "IssueComments", - column: "IssueId", - principalTable: "Issues", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Issues_GitRepositories_RepositoryId", - table: "Issues", - column: "RepositoryId", - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Issues_UserProfiles_AssigneeId", - table: "Issues", - column: "AssigneeId", - principalTable: "UserProfiles", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_Issues_UserProfiles_AuthorId", - table: "Issues", - column: "AuthorId", - principalTable: "UserProfiles", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_IssueComments_Issues_IssueId", - table: "IssueComments"); - - migrationBuilder.DropForeignKey( - name: "FK_Issues_GitRepositories_RepositoryId", - table: "Issues"); - - migrationBuilder.DropForeignKey( - name: "FK_Issues_UserProfiles_AssigneeId", - table: "Issues"); - - migrationBuilder.DropForeignKey( - name: "FK_Issues_UserProfiles_AuthorId", - table: "Issues"); - - migrationBuilder.DropTable( - name: "IssueTags"); - - migrationBuilder.RenameTable( - name: "Issues", - newName: "Issue"); - - migrationBuilder.RenameIndex( - name: "IX_Issues_AuthorId", - table: "Issue", - newName: "IX_Issue_AuthorId"); - - migrationBuilder.RenameIndex( - name: "IX_Issues_AssigneeId", - table: "Issue", - newName: "IX_Issue_AssigneeId"); - - migrationBuilder.AddForeignKey( - name: "FK_Issue_GitRepositories_RepositoryId", - table: "Issue", - column: "RepositoryId", - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Issue_UserProfiles_AssigneeId", - table: "Issue", - column: "AssigneeId", - principalTable: "UserProfiles", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_Issue_UserProfiles_AuthorId", - table: "Issue", - column: "AuthorId", - principalTable: "UserProfiles", - principalColumn: "Id"); - - migrationBuilder.AddForeignKey( - name: "FK_IssueComments_Issue_IssueId", - table: "IssueComments", - column: "IssueId", - principalTable: "Issue", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.Designer.cs deleted file mode 100644 index a3a3a235..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.Designer.cs +++ /dev/null @@ -1,293 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240819170437_RenameTagsToLabels")] - partial class RenameTagsToLabels - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issues", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.Property("LabelId") - .HasColumnType("TEXT"); - - b.HasKey("IssueId", "LabelId") - .HasName("PK_IssueLabel"); - - b.HasIndex("LabelId"); - - b.ToTable("IssueLabels", (string)null); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany() - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Label", "Label") - .WithMany() - .HasForeignKey("LabelId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Issue"); - - b.Navigation("Label"); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("Comments"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.cs deleted file mode 100644 index 6e991d4a..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240819170437_RenameTagsToLabels.cs +++ /dev/null @@ -1,80 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class RenameTagsToLabels : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "IssueTags"); - - migrationBuilder.CreateTable( - name: "IssueLabels", - columns: table => new - { - IssueId = table.Column(type: "TEXT", nullable: false), - LabelId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueLabel", x => new { x.IssueId, x.LabelId }); - table.ForeignKey( - name: "FK_IssueLabels_Issues_IssueId", - column: x => x.IssueId, - principalTable: "Issues", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_IssueLabels_Labels_LabelId", - column: x => x.LabelId, - principalTable: "Labels", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_IssueLabels_LabelId", - table: "IssueLabels", - column: "LabelId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "IssueLabels"); - - migrationBuilder.CreateTable( - name: "IssueTags", - columns: table => new - { - IssueId = table.Column(type: "TEXT", nullable: false), - TagId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueTags", x => new { x.IssueId, x.TagId }); - table.ForeignKey( - name: "FK_IssueTags_Issues_IssueId", - column: x => x.IssueId, - principalTable: "Issues", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_IssueTags_Tags_TagId", - column: x => x.TagId, - principalTable: "Tags", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_IssueTags_TagId", - table: "IssueTags", - column: "TagId"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.Designer.cs deleted file mode 100644 index a08cba84..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.Designer.cs +++ /dev/null @@ -1,395 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240902163715_AddStatuses")] - partial class AddStatuses - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("StatusId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("StatusId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issues", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatuses"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_IssueStatuses_Name"); - - b.HasIndex("RepositoryId"); - - b.ToTable("IssueStatuses", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("FromId") - .HasColumnType("TEXT"); - - b.Property("ToId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatusTranslations"); - - b.HasIndex("ToId"); - - b.HasIndex("FromId", "ToId") - .IsUnique() - .HasDatabaseName("IX_IssueStatusTranslations_FromId_ToId"); - - b.ToTable("IssueStatusTransitions", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.Property("LabelId") - .HasColumnType("TEXT"); - - b.HasKey("IssueId", "LabelId") - .HasName("PK_IssueLabel"); - - b.HasIndex("LabelId"); - - b.ToTable("IssueLabels", (string)null); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "Status") - .WithMany() - .HasForeignKey("StatusId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired() - .HasConstraintName("FK_Issue_Status"); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Status"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany("IssueStatuses") - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.HasOne("Pyro.Domain.Issues.IssueStatus", "From") - .WithMany("FromTransitions") - .HasForeignKey("FromId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "To") - .WithMany("ToTransitions") - .HasForeignKey("ToId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("From"); - - b.Navigation("To"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany() - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Label", "Label") - .WithMany() - .HasForeignKey("LabelId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Issue"); - - b.Navigation("Label"); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Navigation("IssueStatuses"); - - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("Comments"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Navigation("FromTransitions"); - - b.Navigation("ToTransitions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.cs deleted file mode 100644 index 9d9f7a36..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240902163715_AddStatuses.cs +++ /dev/null @@ -1,121 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class AddStatuses : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "StatusId", - table: "Issues", - type: "TEXT", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - - migrationBuilder.CreateTable( - name: "IssueStatuses", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), - Color = table.Column(type: "INTEGER", nullable: false), - RepositoryId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueStatuses", x => x.Id); - table.ForeignKey( - name: "FK_IssueStatuses_GitRepositories_RepositoryId", - column: x => x.RepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "IssueStatusTransitions", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - FromId = table.Column(type: "TEXT", nullable: false), - ToId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_IssueStatusTranslations", x => x.Id); - table.ForeignKey( - name: "FK_IssueStatusTransitions_IssueStatuses_FromId", - column: x => x.FromId, - principalTable: "IssueStatuses", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_IssueStatusTransitions_IssueStatuses_ToId", - column: x => x.ToId, - principalTable: "IssueStatuses", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_Issues_StatusId", - table: "Issues", - column: "StatusId"); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatuses_Name", - table: "IssueStatuses", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatuses_RepositoryId", - table: "IssueStatuses", - column: "RepositoryId"); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatusTransitions_ToId", - table: "IssueStatusTransitions", - column: "ToId"); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatusTranslations_FromId_ToId", - table: "IssueStatusTransitions", - columns: new[] { "FromId", "ToId" }, - unique: true); - - migrationBuilder.AddForeignKey( - name: "FK_Issue_Status", - table: "Issues", - column: "StatusId", - principalTable: "IssueStatuses", - principalColumn: "Id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_Issue_Status", - table: "Issues"); - - migrationBuilder.DropTable( - name: "IssueStatusTransitions"); - - migrationBuilder.DropTable( - name: "IssueStatuses"); - - migrationBuilder.DropIndex( - name: "IX_Issues_StatusId", - table: "Issues"); - - migrationBuilder.DropColumn( - name: "StatusId", - table: "Issues"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.Designer.cs deleted file mode 100644 index 9f6ded32..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.Designer.cs +++ /dev/null @@ -1,400 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240908182802_AddIsLocked")] - partial class AddIsLocked - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IsLocked") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("StatusId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("StatusId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issues", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatuses"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_IssueStatuses_Name"); - - b.HasIndex("RepositoryId"); - - b.ToTable("IssueStatuses", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("FromId") - .HasColumnType("TEXT"); - - b.Property("ToId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatusTranslations"); - - b.HasIndex("ToId"); - - b.HasIndex("FromId", "ToId") - .IsUnique() - .HasDatabaseName("IX_IssueStatusTranslations_FromId_ToId"); - - b.ToTable("IssueStatusTransitions", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.Property("LabelId") - .HasColumnType("TEXT"); - - b.HasKey("IssueId", "LabelId") - .HasName("PK_IssueLabel"); - - b.HasIndex("LabelId"); - - b.ToTable("IssueLabels", (string)null); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "Status") - .WithMany() - .HasForeignKey("StatusId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired() - .HasConstraintName("FK_Issue_Status"); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Status"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany("IssueStatuses") - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.HasOne("Pyro.Domain.Issues.IssueStatus", "From") - .WithMany("FromTransitions") - .HasForeignKey("FromId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "To") - .WithMany("ToTransitions") - .HasForeignKey("ToId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("From"); - - b.Navigation("To"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany() - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Label", "Label") - .WithMany() - .HasForeignKey("LabelId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Issue"); - - b.Navigation("Label"); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Navigation("IssueStatuses"); - - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("Comments"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Navigation("FromTransitions"); - - b.Navigation("ToTransitions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.cs deleted file mode 100644 index 847589b3..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240908182802_AddIsLocked.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class AddIsLocked : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsLocked", - table: "Issues", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsLocked", - table: "Issues"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.Designer.cs deleted file mode 100644 index 26842113..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.Designer.cs +++ /dev/null @@ -1,593 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.Issues.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - [DbContext(typeof(IssuesDbContext))] - [Migration("20240917153755_AddIssueChangeLogs")] - partial class AddIssueChangeLogs - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AssigneeId") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IsLocked") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("IssueNumber") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("StatusId") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Issue"); - - b.HasIndex("AssigneeId"); - - b.HasIndex("AuthorId"); - - b.HasIndex("StatusId"); - - b.HasIndex("RepositoryId", "IssueNumber") - .IsUnique() - .HasDatabaseName("IX_Issue_RepositoryId_Number"); - - b.ToTable("Issues", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueChangeLog", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueChangeLog"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable((string)null); - - b.UseTpcMappingStrategy(); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AuthorId") - .HasColumnType("TEXT"); - - b.Property("Content") - .IsRequired() - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueComment"); - - b.HasIndex("AuthorId"); - - b.HasIndex("IssueId"); - - b.ToTable("IssueComments", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("IsDisabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatuses"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_IssueStatuses_Name"); - - b.HasIndex("RepositoryId"); - - b.ToTable("IssueStatuses", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("FromId") - .HasColumnType("TEXT"); - - b.Property("ToId") - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_IssueStatusTranslations"); - - b.HasIndex("ToId"); - - b.HasIndex("FromId", "ToId") - .IsUnique() - .HasDatabaseName("IX_IssueStatusTranslations_FromId_ToId"); - - b.ToTable("IssueStatusTransitions", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("IsDisabled") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.Property("IssueId") - .HasColumnType("TEXT"); - - b.Property("LabelId") - .HasColumnType("TEXT"); - - b.HasKey("IssueId", "LabelId") - .HasName("PK_IssueLabel"); - - b.HasIndex("LabelId"); - - b.ToTable("IssueLabels", (string)null); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.Property("RepositoryId") - .HasColumnType("TEXT"); - - b.Property("Number") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(0); - - b.HasKey("RepositoryId") - .HasName("PK_IssueNumberTracker"); - - b.ToTable("IssueNumberTracker", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueAssigneeChangeLog", b => - { - b.HasBaseType("Pyro.Domain.Issues.IssueChangeLog"); - - b.Property("NewAssigneeId") - .HasColumnType("TEXT") - .HasColumnName("NewAssigneeId"); - - b.Property("OldAssigneeId") - .HasColumnType("TEXT") - .HasColumnName("OldAssigneeId"); - - b.HasIndex("NewAssigneeId"); - - b.HasIndex("OldAssigneeId"); - - b.ToTable("IssueAssigneeChangeLogs", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueLabelChangeLog", b => - { - b.HasBaseType("Pyro.Domain.Issues.IssueChangeLog"); - - b.Property("NewLabelId") - .HasColumnType("TEXT") - .HasColumnName("NewLabelId"); - - b.Property("OldLabelId") - .HasColumnType("TEXT") - .HasColumnName("OldLabelId"); - - b.HasIndex("NewLabelId"); - - b.HasIndex("OldLabelId"); - - b.ToTable("IssueLabelChangeLogs", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueLockChangeLog", b => - { - b.HasBaseType("Pyro.Domain.Issues.IssueChangeLog"); - - b.Property("NewValue") - .HasColumnType("INTEGER") - .HasColumnName("NewValue"); - - b.Property("OldValue") - .HasColumnType("INTEGER") - .HasColumnName("OldValue"); - - b.ToTable("IssueLockChangeLogs", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusChangeLog", b => - { - b.HasBaseType("Pyro.Domain.Issues.IssueChangeLog"); - - b.Property("NewStatusId") - .HasColumnType("TEXT") - .HasColumnName("NewStatusId"); - - b.Property("OldStatusId") - .HasColumnType("TEXT") - .HasColumnName("OldStatusId"); - - b.HasIndex("NewStatusId"); - - b.HasIndex("OldStatusId"); - - b.ToTable("IssueStatusChangeLogs", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueTitleChangeLog", b => - { - b.HasBaseType("Pyro.Domain.Issues.IssueChangeLog"); - - b.Property("NewTitle") - .HasColumnType("TEXT") - .HasColumnName("NewValue"); - - b.Property("OldTitle") - .HasColumnType("TEXT") - .HasColumnName("OldValue"); - - b.ToTable("IssueTitleChangeLog"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "Status") - .WithMany() - .HasForeignKey("StatusId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired() - .HasConstraintName("FK_Issue_Status"); - - b.Navigation("Assignee"); - - b.Navigation("Author"); - - b.Navigation("Status"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueChangeLog", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("ChangeLogs") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueComment", b => - { - b.HasOne("Pyro.Domain.Issues.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany("Comments") - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Author"); - - b.Navigation("Issue"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", "Repository") - .WithMany("IssueStatuses") - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusTransition", b => - { - b.HasOne("Pyro.Domain.Issues.IssueStatus", "From") - .WithMany("FromTransitions") - .HasForeignKey("FromId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "To") - .WithMany("ToTransitions") - .HasForeignKey("ToId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("From"); - - b.Navigation("To"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Label", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueLabel", b => - { - b.HasOne("Pyro.Domain.Issues.Issue", "Issue") - .WithMany() - .HasForeignKey("IssueId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.HasOne("Pyro.Domain.Issues.Label", "Label") - .WithMany() - .HasForeignKey("LabelId") - .OnDelete(DeleteBehavior.ClientCascade) - .IsRequired(); - - b.Navigation("Issue"); - - b.Navigation("Label"); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Issues.DataAccess.Configurations.IssueNumberTrackerConfiguration+IssueNumberTracker", b => - { - b.HasOne("Pyro.Domain.Issues.GitRepository", null) - .WithMany() - .HasForeignKey("RepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("FK_IssueNumberTracker_Repository"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueAssigneeChangeLog", b => - { - b.HasOne("Pyro.Domain.Issues.User", "NewAssignee") - .WithMany() - .HasForeignKey("NewAssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.User", "OldAssignee") - .WithMany() - .HasForeignKey("OldAssigneeId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("NewAssignee"); - - b.Navigation("OldAssignee"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueLabelChangeLog", b => - { - b.HasOne("Pyro.Domain.Issues.Label", "NewLabel") - .WithMany() - .HasForeignKey("NewLabelId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.Label", "OldLabel") - .WithMany() - .HasForeignKey("OldLabelId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("NewLabel"); - - b.Navigation("OldLabel"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatusChangeLog", b => - { - b.HasOne("Pyro.Domain.Issues.IssueStatus", "NewStatus") - .WithMany() - .HasForeignKey("NewStatusId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Pyro.Domain.Issues.IssueStatus", "OldStatus") - .WithMany() - .HasForeignKey("OldStatusId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("NewStatus"); - - b.Navigation("OldStatus"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => - { - b.Navigation("IssueStatuses"); - - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.Issue", b => - { - b.Navigation("ChangeLogs"); - - b.Navigation("Comments"); - }); - - modelBuilder.Entity("Pyro.Domain.Issues.IssueStatus", b => - { - b.Navigation("FromTransitions"); - - b.Navigation("ToTransitions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.cs deleted file mode 100644 index fffb12d8..00000000 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Issues.Migrations -{ - /// - public partial class UpdateIssueStatusUniqueIndex : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_IssueStatuses_Name", - table: "IssueStatuses"); - - migrationBuilder.DropIndex( - name: "IX_IssueStatuses_RepositoryId", - table: "IssueStatuses"); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatuses_Name", - table: "IssueStatuses", - columns: new[] { "RepositoryId", "Name" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_IssueStatuses_Name", - table: "IssueStatuses"); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatuses_Name", - table: "IssueStatuses", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_IssueStatuses_RepositoryId", - table: "IssueStatuses", - column: "RepositoryId"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.Designer.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.Designer.cs similarity index 99% rename from Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.Designer.cs rename to Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.Designer.cs index 999f8e2a..39b29bc9 100644 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241011095604_UpdateIssueStatusUniqueIndex.Designer.cs +++ b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.Designer.cs @@ -11,14 +11,14 @@ namespace Pyro.Infrastructure.Issues.Migrations { [DbContext(typeof(IssuesDbContext))] - [Migration("20241011095604_UpdateIssueStatusUniqueIndex")] - partial class UpdateIssueStatusUniqueIndex + [Migration("20241118140145_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => { diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.cs similarity index 55% rename from Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.cs rename to Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.cs index d113c3fc..5ba9a887 100644 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20240917153755_AddIssueChangeLogs.cs +++ b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/20241118140145_Initial.cs @@ -7,17 +7,111 @@ namespace Pyro.Infrastructure.Issues.Migrations { /// - public partial class AddIssueChangeLogs : Migration + public partial class Initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "IsDisabled", - table: "IssueStatuses", - type: "INTEGER", - nullable: false, - defaultValue: false); + migrationBuilder.CreateTable( + name: "IssueNumberTracker", + columns: table => new + { + RepositoryId = table.Column(type: "TEXT", nullable: false), + Number = table.Column(type: "INTEGER", nullable: false, defaultValue: 0) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueNumberTracker", x => x.RepositoryId); + table.ForeignKey( + name: "FK_IssueNumberTracker_Repository", + column: x => x.RepositoryId, + principalTable: "GitRepositories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "IssueStatuses", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Color = table.Column(type: "INTEGER", nullable: false), + RepositoryId = table.Column(type: "TEXT", nullable: false), + IsDisabled = table.Column(type: "INTEGER", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueStatuses", x => x.Id); + table.ForeignKey( + name: "FK_IssueStatuses_GitRepositories_RepositoryId", + column: x => x.RepositoryId, + principalTable: "GitRepositories", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Issues", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + IssueNumber = table.Column(type: "INTEGER", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), + StatusId = table.Column(type: "TEXT", nullable: false), + RepositoryId = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "INTEGER", nullable: false), + AssigneeId = table.Column(type: "TEXT", nullable: true), + IsLocked = table.Column(type: "INTEGER", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Issue", x => x.Id); + table.ForeignKey( + name: "FK_Issue_Status", + column: x => x.StatusId, + principalTable: "IssueStatuses", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Issues_GitRepositories_RepositoryId", + column: x => x.RepositoryId, + principalTable: "GitRepositories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Issues_UserProfiles_AssigneeId", + column: x => x.AssigneeId, + principalTable: "UserProfiles", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Issues_UserProfiles_AuthorId", + column: x => x.AuthorId, + principalTable: "UserProfiles", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "IssueStatusTransitions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + FromId = table.Column(type: "TEXT", nullable: false), + ToId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueStatusTranslations", x => x.Id); + table.ForeignKey( + name: "FK_IssueStatusTransitions_IssueStatuses_FromId", + column: x => x.FromId, + principalTable: "IssueStatuses", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_IssueStatusTransitions_IssueStatuses_ToId", + column: x => x.ToId, + principalTable: "IssueStatuses", + principalColumn: "Id"); + }); migrationBuilder.CreateTable( name: "IssueAssigneeChangeLogs", @@ -55,6 +149,32 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id"); }); + migrationBuilder.CreateTable( + name: "IssueComments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + IssueId = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueComment", x => x.Id); + table.ForeignKey( + name: "FK_IssueComments_Issues_IssueId", + column: x => x.IssueId, + principalTable: "Issues", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_IssueComments_UserProfiles_AuthorId", + column: x => x.AuthorId, + principalTable: "UserProfiles", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "IssueLabelChangeLogs", columns: table => new @@ -91,6 +211,28 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id"); }); + migrationBuilder.CreateTable( + name: "IssueLabels", + columns: table => new + { + IssueId = table.Column(type: "TEXT", nullable: false), + LabelId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IssueLabel", x => new { x.IssueId, x.LabelId }); + table.ForeignKey( + name: "FK_IssueLabels_Issues_IssueId", + column: x => x.IssueId, + principalTable: "Issues", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_IssueLabels_Labels_LabelId", + column: x => x.LabelId, + principalTable: "Labels", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "IssueLockChangeLogs", columns: table => new @@ -199,6 +341,16 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "IssueAssigneeChangeLogs", column: "OldAssigneeId"); + migrationBuilder.CreateIndex( + name: "IX_IssueComments_AuthorId", + table: "IssueComments", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_IssueComments_IssueId", + table: "IssueComments", + column: "IssueId"); + migrationBuilder.CreateIndex( name: "IX_IssueLabelChangeLogs_AuthorId", table: "IssueLabelChangeLogs", @@ -219,6 +371,11 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "IssueLabelChangeLogs", column: "OldLabelId"); + migrationBuilder.CreateIndex( + name: "IX_IssueLabels_LabelId", + table: "IssueLabels", + column: "LabelId"); + migrationBuilder.CreateIndex( name: "IX_IssueLockChangeLogs_AuthorId", table: "IssueLockChangeLogs", @@ -229,6 +386,27 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "IssueLockChangeLogs", column: "IssueId"); + migrationBuilder.CreateIndex( + name: "IX_Issue_RepositoryId_Number", + table: "Issues", + columns: new[] { "RepositoryId", "IssueNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Issues_AssigneeId", + table: "Issues", + column: "AssigneeId"); + + migrationBuilder.CreateIndex( + name: "IX_Issues_AuthorId", + table: "Issues", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Issues_StatusId", + table: "Issues", + column: "StatusId"); + migrationBuilder.CreateIndex( name: "IX_IssueStatusChangeLogs_AuthorId", table: "IssueStatusChangeLogs", @@ -249,6 +427,23 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "IssueStatusChangeLogs", column: "OldStatusId"); + migrationBuilder.CreateIndex( + name: "IX_IssueStatuses_Name", + table: "IssueStatuses", + columns: new[] { "RepositoryId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_IssueStatusTransitions_ToId", + table: "IssueStatusTransitions", + column: "ToId"); + + migrationBuilder.CreateIndex( + name: "IX_IssueStatusTranslations_FromId_ToId", + table: "IssueStatusTransitions", + columns: new[] { "FromId", "ToId" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_IssueTitleChangeLog_AuthorId", table: "IssueTitleChangeLog", @@ -266,21 +461,35 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "IssueAssigneeChangeLogs"); + migrationBuilder.DropTable( + name: "IssueComments"); + migrationBuilder.DropTable( name: "IssueLabelChangeLogs"); + migrationBuilder.DropTable( + name: "IssueLabels"); + migrationBuilder.DropTable( name: "IssueLockChangeLogs"); + migrationBuilder.DropTable( + name: "IssueNumberTracker"); + migrationBuilder.DropTable( name: "IssueStatusChangeLogs"); + migrationBuilder.DropTable( + name: "IssueStatusTransitions"); + migrationBuilder.DropTable( name: "IssueTitleChangeLog"); - migrationBuilder.DropColumn( - name: "IsDisabled", - table: "IssueStatuses"); + migrationBuilder.DropTable( + name: "Issues"); + + migrationBuilder.DropTable( + name: "IssueStatuses"); } } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/IssuesDbContextModelSnapshot.cs b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/IssuesDbContextModelSnapshot.cs index a2329683..21bf4e18 100644 --- a/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/IssuesDbContextModelSnapshot.cs +++ b/Pyro.Api/Pyro.Infrastructure.Issues/Migrations/IssuesDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class IssuesDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Pyro.Domain.Issues.GitRepository", b => { diff --git a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/SeedData.cs b/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/SeedData.cs deleted file mode 100644 index 2d798d1f..00000000 --- a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/SeedData.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using Pyro.Domain.UserProfiles; - -namespace Pyro.Infrastructure.DataAccess.Configurations; - -public static class SeedData -{ - public static readonly IReadOnlyCollection UserProfiles; - - static SeedData() - { - UserProfiles = - [ - new UserProfile - { - Id = UserProfile.Pyro, - Name = "Pyro", - } - ]; - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserAvatarConfiguration.cs b/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserAvatarConfiguration.cs deleted file mode 100644 index 7ba52a69..00000000 --- a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserAvatarConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Pyro.Domain.UserProfiles; - -namespace Pyro.Infrastructure.DataAccess.Configurations.UserProfiles; - -internal class UserAvatarConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("UserAvatars"); - - builder.HasKey(x => x.Id) - .HasName("PK_UserAvatar"); - - builder.Property(x => x.Id) - .IsRequired(); - - builder.Property(x => x.Image) - .IsRequired(); - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserConfiguration.cs b/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserConfiguration.cs deleted file mode 100644 index 5d73bc29..00000000 --- a/Pyro.Api/Pyro.Infrastructure/DataAccess/Configurations/UserProfiles/UserConfiguration.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Pyro.Domain.UserProfiles; - -namespace Pyro.Infrastructure.DataAccess.Configurations.UserProfiles; - -internal class UserConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("Users", b => b.ExcludeFromMigrations()); - - builder.Property(x => x.Id) - .IsRequired(); - - builder.Property(x => x.Email) - .HasColumnName("Login") - .IsRequired() - .HasMaxLength(32); - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/DataAccess/UserProfileRepository.cs b/Pyro.Api/Pyro.Infrastructure/DataAccess/UserProfileRepository.cs deleted file mode 100644 index 4bb58fc5..00000000 --- a/Pyro.Api/Pyro.Infrastructure/DataAccess/UserProfileRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using Microsoft.EntityFrameworkCore; -using Pyro.Domain.UserProfiles; - -namespace Pyro.Infrastructure.DataAccess; - -internal class UserProfileRepository : IUserProfileRepository -{ - private readonly PyroDbContext dbContext; - - public UserProfileRepository(PyroDbContext dbContext) - => this.dbContext = dbContext; - - public async Task GetUserProfile(Guid userId, CancellationToken cancellationToken) - { - var profile = await dbContext - .Set() - .Include(x => x.Avatar) - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - - return profile; - } - - public async Task AddUserProfile(UserProfile userProfile, CancellationToken cancellationToken) - { - await dbContext - .Set() - .AddAsync(userProfile, cancellationToken); - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/GitService.cs b/Pyro.Api/Pyro.Infrastructure/GitService.cs index 5842cc2a..a9e81d09 100644 --- a/Pyro.Api/Pyro.Infrastructure/GitService.cs +++ b/Pyro.Api/Pyro.Infrastructure/GitService.cs @@ -8,7 +8,6 @@ using Pyro.Domain.Git; using Pyro.Domain.GitRepositories; using Pyro.Domain.Shared.Exceptions; -using Pyro.Domain.UserProfiles; namespace Pyro.Infrastructure; @@ -19,18 +18,15 @@ internal class GitService : IGitService private readonly GitOptions options; private readonly ILogger logger; private readonly TimeProvider timeProvider; - private readonly IUserProfileRepository profileRepository; public GitService( IOptions options, ILogger logger, - TimeProvider timeProvider, - IUserProfileRepository profileRepository) + TimeProvider timeProvider) { this.options = options.Value; this.logger = logger; this.timeProvider = timeProvider; - this.profileRepository = profileRepository; } private string GetGitPath(GitRepository repository) @@ -54,9 +50,6 @@ public async Task InitializeRepository( GitRepository gitRepo, CancellationToken cancellationToken = default) { - var pyroUser = await profileRepository.GetUserProfile(UserProfile.Pyro, cancellationToken) ?? - throw new DomainException("Pyro user not found"); - var gitPath = GetGitPath(gitRepo); gitPath = Repository.Init(gitPath, true); @@ -74,7 +67,10 @@ exec git update-server-info var clonePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); clonePath = Repository.Clone(gitPath, clonePath); - var identity = new Identity(pyroUser.Name, pyroUser.User.Email); + // TODO: use user identity + const string pyroName = "Pyro"; + const string pyroEmail = "pyro@localhost.local"; + var identity = new Identity(pyroName, pyroEmail); var signature = new Signature(identity, timeProvider.GetUtcNow()); using var repo = new Repository(clonePath, new RepositoryOptions { Identity = identity }); diff --git a/Pyro.Api/Pyro.Infrastructure/Messaging/Bus.cs b/Pyro.Api/Pyro.Infrastructure/Messaging/Bus.cs index 6945b0bd..536d7f60 100644 --- a/Pyro.Api/Pyro.Infrastructure/Messaging/Bus.cs +++ b/Pyro.Api/Pyro.Infrastructure/Messaging/Bus.cs @@ -53,6 +53,7 @@ public async IAsyncEnumerable GetBatch( .OrderBy(x => x.CreatedAt) .Take(batchSize) .AsNoTracking() + .TagWith("NO_LOGGING") .AsAsyncEnumerable(); await foreach (var outboxMessage in outboxMessages) diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.Designer.cs deleted file mode 100644 index efec15e4..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.Designer.cs +++ /dev/null @@ -1,207 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - [DbContext(typeof(PyroDbContext))] - [Migration("20240818120634_AddTags")] - partial class AddTags - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FriendlyName") - .HasColumnType("TEXT"); - - b.Property("Xml") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultBranch") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Tag", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Tag"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Tag_Name"); - - b.ToTable("Tags", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Email = "pyro@localhost.local", - Name = "Pyro" - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("Message") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Retries") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_OutboxMessages"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("IX_OutboxMessages_CreatedAt"); - - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Tag", b => - { - b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") - .WithMany("Tags") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GitRepository"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Navigation("Tags"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.cs deleted file mode 100644 index e36fa669..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240818120634_AddTags.cs +++ /dev/null @@ -1,54 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - /// - public partial class AddTags : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Tags", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), - Color = table.Column(type: "INTEGER", nullable: false), - GitRepositoryId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tag", x => x.Id); - table.ForeignKey( - name: "FK_Tags_GitRepositories_GitRepositoryId", - column: x => x.GitRepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Tag_Name", - table: "Tags", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tags_GitRepositoryId", - table: "Tags", - column: "GitRepositoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Tags"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.Designer.cs deleted file mode 100644 index 6ec5c0b6..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.Designer.cs +++ /dev/null @@ -1,207 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - [DbContext(typeof(PyroDbContext))] - [Migration("20240819170348_RenameTagsToLabels")] - partial class RenameTagsToLabels - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FriendlyName") - .HasColumnType("TEXT"); - - b.Property("Xml") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultBranch") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Email = "pyro@localhost.local", - Name = "Pyro" - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("Message") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Retries") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_OutboxMessages"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("IX_OutboxMessages_CreatedAt"); - - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GitRepository"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.cs deleted file mode 100644 index 248d8c98..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240819170348_RenameTagsToLabels.cs +++ /dev/null @@ -1,88 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - /// - public partial class RenameTagsToLabels : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Tags"); - - migrationBuilder.CreateTable( - name: "Labels", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), - Color = table.Column(type: "INTEGER", nullable: false), - GitRepositoryId = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Label", x => x.Id); - table.ForeignKey( - name: "FK_Labels_GitRepositories_GitRepositoryId", - column: x => x.GitRepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Label_Name", - table: "Labels", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Labels_GitRepositoryId", - table: "Labels", - column: "GitRepositoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Labels"); - - migrationBuilder.CreateTable( - name: "Tags", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - GitRepositoryId = table.Column(type: "TEXT", nullable: false), - Color = table.Column(type: "INTEGER", nullable: false), - Name = table.Column(type: "TEXT", maxLength: 50, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tag", x => x.Id); - table.ForeignKey( - name: "FK_Tags_GitRepositories_GitRepositoryId", - column: x => x.GitRepositoryId, - principalTable: "GitRepositories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Tag_Name", - table: "Tags", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Tags_GitRepositoryId", - table: "Tags", - column: "GitRepositoryId"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.Designer.cs deleted file mode 100644 index d9b7bfc1..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.Designer.cs +++ /dev/null @@ -1,212 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - [DbContext(typeof(PyroDbContext))] - [Migration("20240916092147_AddIsEnabledIsDisabled")] - partial class AddIsEnabledIsDisabled - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FriendlyName") - .HasColumnType("TEXT"); - - b.Property("Xml") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultBranch") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("IsDisabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Email = "pyro@localhost.local", - Name = "Pyro" - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("Message") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Retries") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_OutboxMessages"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("IX_OutboxMessages_CreatedAt"); - - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GitRepository"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.cs deleted file mode 100644 index edec8e9b..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240916092147_AddIsEnabledIsDisabled.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - /// - public partial class AddIsEnabledIsDisabled : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsDisabled", - table: "Labels", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsDisabled", - table: "Labels"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.Designer.cs deleted file mode 100644 index 0ad4216f..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.Designer.cs +++ /dev/null @@ -1,210 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - [DbContext(typeof(PyroDbContext))] - [Migration("20241010161006_UpdateLabelNameUniqueIndex")] - partial class UpdateLabelNameUniqueIndex - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FriendlyName") - .HasColumnType("TEXT"); - - b.Property("Xml") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultBranch") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("IsDisabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId", "Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Email = "pyro@localhost.local", - Name = "Pyro" - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("Message") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Retries") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_OutboxMessages"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("IX_OutboxMessages_CreatedAt"); - - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GitRepository"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.cs deleted file mode 100644 index 6fef6daa..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20241010161006_UpdateLabelNameUniqueIndex.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - /// - public partial class UpdateLabelNameUniqueIndex : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Label_Name", - table: "Labels"); - - migrationBuilder.DropIndex( - name: "IX_Labels_GitRepositoryId", - table: "Labels"); - - migrationBuilder.CreateIndex( - name: "IX_Label_Name", - table: "Labels", - columns: new[] { "GitRepositoryId", "Name" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Label_Name", - table: "Labels"); - - migrationBuilder.CreateIndex( - name: "IX_Label_Name", - table: "Labels", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Labels_GitRepositoryId", - table: "Labels", - column: "GitRepositoryId"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.Designer.cs deleted file mode 100644 index 74ba9c43..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.Designer.cs +++ /dev/null @@ -1,236 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Pyro.Infrastructure.DataAccess; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - [DbContext(typeof(PyroDbContext))] - [Migration("20241028213258_RemoveEmailField")] - partial class RemoveEmailField - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); - - modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("FriendlyName") - .HasColumnType("TEXT"); - - b.Property("Xml") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("DataProtectionKeys", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("DefaultBranch") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.HasKey("Id") - .HasName("PK_GitRepository"); - - b.HasIndex("Name") - .IsUnique() - .HasDatabaseName("IX_GitRepository_Name"); - - b.ToTable("GitRepositories", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("GitRepositoryId") - .HasColumnType("TEXT"); - - b.Property("IsDisabled") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasDefaultValue(false); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_Label"); - - b.HasIndex("GitRepositoryId", "Name") - .IsUnique() - .HasDatabaseName("IX_Label_Name"); - - b.ToTable("Labels", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.User", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT") - .HasColumnName("Login"); - - b.HasKey("Id"); - - b.ToTable("Users", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Name = "Pyro" - }); - }); - - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("INTEGER"); - - b.Property("Message") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Retries") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_OutboxMessages"); - - b.HasIndex("CreatedAt") - .HasDatabaseName("IX_OutboxMessages_CreatedAt"); - - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => - { - b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") - .WithMany("Labels") - .HasForeignKey("GitRepositoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GitRepository"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.User", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("User") - .HasForeignKey("Pyro.Domain.UserProfiles.User", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => - { - b.Navigation("Labels"); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - - b.Navigation("User") - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.cs deleted file mode 100644 index 8bb1907b..00000000 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20241028213258_RemoveEmailField.cs +++ /dev/null @@ -1,38 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Pyro.Infrastructure.Migrations -{ - /// - public partial class RemoveEmailField : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Email", - table: "UserProfiles"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Email", - table: "UserProfiles", - type: "TEXT", - maxLength: 200, - nullable: true); - - migrationBuilder.UpdateData( - table: "UserProfiles", - keyColumn: "Id", - keyValue: new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - column: "Email", - value: "pyro@localhost.local"); - } - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.Designer.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.Designer.cs similarity index 69% rename from Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.Designer.cs rename to Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.Designer.cs index d83d71b7..b0c909e9 100644 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.Designer.cs +++ b/Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.Designer.cs @@ -11,14 +11,14 @@ namespace Pyro.Infrastructure.Migrations { [DbContext(typeof(PyroDbContext))] - [Migration("20240812212151_Initial")] + [Migration("20241118140058_Initial")] partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => { @@ -69,51 +69,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("GitRepositories", (string)null); }); - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => + modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => { b.Property("Id") .HasColumnType("TEXT"); - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); + b.Property("Color") + .HasColumnType("INTEGER"); - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") + b.Property("GitRepositoryId") .HasColumnType("TEXT"); - b.Property("Email") - .HasMaxLength(200) - .HasColumnType("TEXT"); + b.Property("IsDisabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("TEXT"); - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - b.HasKey("Id") - .HasName("PK_UserProfile"); + .HasName("PK_Label"); - b.ToTable("UserProfiles", (string)null); + b.HasIndex("GitRepositoryId", "Name") + .IsUnique() + .HasDatabaseName("IX_Label_Name"); - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Email = "pyro@localhost.local", - Name = "Pyro" - }); + b.ToTable("Labels", (string)null); }); modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => @@ -144,18 +128,20 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("OutboxMessages", (string)null); }); - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => + modelBuilder.Entity("Pyro.Domain.GitRepositories.Label", b => { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") + b.HasOne("Pyro.Domain.GitRepositories.GitRepository", "GitRepository") + .WithMany("Labels") + .HasForeignKey("GitRepositoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("GitRepository"); }); - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => + modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => { - b.Navigation("Avatar"); + b.Navigation("Labels"); }); #pragma warning restore 612, 618 } diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.cs similarity index 74% rename from Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.cs rename to Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.cs index c15ec6e5..9f6ecfef 100644 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/20240812212151_Initial.cs +++ b/Pyro.Api/Pyro.Infrastructure/Migrations/20241118140058_Initial.cs @@ -57,48 +57,38 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateTable( - name: "UserProfiles", + name: "Labels", columns: table => new { Id = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), - Email = table.Column(type: "TEXT", maxLength: 200, nullable: true), - Status = table.Column(type: "TEXT", maxLength: 200, nullable: true) + Color = table.Column(type: "INTEGER", nullable: false), + GitRepositoryId = table.Column(type: "TEXT", nullable: false), + IsDisabled = table.Column(type: "INTEGER", nullable: false, defaultValue: false) }, constraints: table => { - table.PrimaryKey("PK_UserProfile", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "UserAvatars", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Image = table.Column(type: "BLOB", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserAvatar", x => x.Id); + table.PrimaryKey("PK_Label", x => x.Id); table.ForeignKey( - name: "FK_UserAvatars_UserProfiles_Id", - column: x => x.Id, - principalTable: "UserProfiles", + name: "FK_Labels_GitRepositories_GitRepositoryId", + column: x => x.GitRepositoryId, + principalTable: "GitRepositories", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); - migrationBuilder.InsertData( - table: "UserProfiles", - columns: new[] { "Id", "Email", "Name", "Status" }, - values: new object[] { new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), "pyro@localhost.local", "Pyro", null }); - migrationBuilder.CreateIndex( name: "IX_GitRepository_Name", table: "GitRepositories", column: "Name", unique: true); + migrationBuilder.CreateIndex( + name: "IX_Label_Name", + table: "Labels", + columns: new[] { "GitRepositoryId", "Name" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_OutboxMessages_CreatedAt", table: "OutboxMessages", @@ -112,16 +102,13 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "DataProtectionKeys"); migrationBuilder.DropTable( - name: "GitRepositories"); + name: "Labels"); migrationBuilder.DropTable( name: "OutboxMessages"); migrationBuilder.DropTable( - name: "UserAvatars"); - - migrationBuilder.DropTable( - name: "UserProfiles"); + name: "GitRepositories"); } } } \ No newline at end of file diff --git a/Pyro.Api/Pyro.Infrastructure/Migrations/PyroDbContextModelSnapshot.cs b/Pyro.Api/Pyro.Infrastructure/Migrations/PyroDbContextModelSnapshot.cs index 65ba4c5b..56fd4426 100644 --- a/Pyro.Api/Pyro.Infrastructure/Migrations/PyroDbContextModelSnapshot.cs +++ b/Pyro.Api/Pyro.Infrastructure/Migrations/PyroDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class PyroDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => { @@ -97,67 +97,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Labels", (string)null); }); - modelBuilder.Entity("Pyro.Domain.UserProfiles.User", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT") - .HasColumnName("Login"); - - b.HasKey("Id"); - - b.ToTable("Users", null, t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Image") - .IsRequired() - .HasColumnType("BLOB"); - - b.HasKey("Id") - .HasName("PK_UserAvatar"); - - b.ToTable("UserAvatars", (string)null); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("Status") - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id") - .HasName("PK_UserProfile"); - - b.ToTable("UserProfiles", (string)null); - - b.HasData( - new - { - Id = new Guid("f9ba057a-35b0-4d10-8326-702d8f7ec966"), - Name = "Pyro" - }); - }); - modelBuilder.Entity("Pyro.Infrastructure.Messaging.OutboxMessage", b => { b.Property("Id") @@ -197,36 +136,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("GitRepository"); }); - modelBuilder.Entity("Pyro.Domain.UserProfiles.User", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("User") - .HasForeignKey("Pyro.Domain.UserProfiles.User", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserAvatar", b => - { - b.HasOne("Pyro.Domain.UserProfiles.UserProfile", null) - .WithOne("Avatar") - .HasForeignKey("Pyro.Domain.UserProfiles.UserAvatar", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Pyro.Domain.GitRepositories.GitRepository", b => { b.Navigation("Labels"); }); - - modelBuilder.Entity("Pyro.Domain.UserProfiles.UserProfile", b => - { - b.Navigation("Avatar"); - - b.Navigation("User") - .IsRequired(); - }); #pragma warning restore 612, 618 } } diff --git a/Pyro.Api/Pyro.Infrastructure/ServiceCollectionExtensions.cs b/Pyro.Api/Pyro.Infrastructure/ServiceCollectionExtensions.cs index 8b3e4612..b95115cd 100644 --- a/Pyro.Api/Pyro.Infrastructure/ServiceCollectionExtensions.cs +++ b/Pyro.Api/Pyro.Infrastructure/ServiceCollectionExtensions.cs @@ -8,7 +8,6 @@ using Pyro.Domain.Git; using Pyro.Domain.GitRepositories; using Pyro.Domain.Shared.Messaging; -using Pyro.Domain.UserProfiles; using Pyro.Infrastructure.DataAccess; using Pyro.Infrastructure.Messaging; using Pyro.Infrastructure.Shared.DataAccess; @@ -43,8 +42,7 @@ public static IHostApplicationBuilder AddInfrastructure(this IHostApplicationBui .AddScoped(sp => sp.GetRequiredService()) .AddScoped() .AddScoped() - .AddScoped() - .AddScoped(); + .AddScoped(); return builder; } diff --git a/Pyro.Api/Pyro/BackgroundServices/NotifyExpiringPasswordsBackgroundService.cs b/Pyro.Api/Pyro/BackgroundServices/NotifyExpiringPasswordsBackgroundService.cs new file mode 100644 index 00000000..47eacc44 --- /dev/null +++ b/Pyro.Api/Pyro/BackgroundServices/NotifyExpiringPasswordsBackgroundService.cs @@ -0,0 +1,53 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. + +using MediatR; +using Microsoft.Extensions.Options; +using Pyro.Domain.Identity.Commands; + +namespace Pyro.BackgroundServices; + +internal class NotifyExpiringPasswordsBackgroundService : BackgroundService +{ + private readonly ILogger logger; + private readonly IHostApplicationLifetime applicationLifetime; + private readonly IServiceProvider serviceProvider; + private readonly NotifyExpiringPasswordsOptions options; + + public NotifyExpiringPasswordsBackgroundService( + ILogger logger, + IHostApplicationLifetime applicationLifetime, + IServiceProvider serviceProvider, + IOptions options) + { + this.logger = logger; + this.applicationLifetime = applicationLifetime; + this.serviceProvider = serviceProvider; + this.options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await applicationLifetime.WaitForApplicationStarted(stoppingToken)) + return; + + logger.LogInformation("NotifyExpiringPasswords started."); + + using var scope = serviceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + + while (!stoppingToken.IsCancellationRequested) + { + await mediator.Send(new NotifyExpiringPasswords(), stoppingToken); + + await Task.Delay(options.Interval, stoppingToken); + } + } +} + +internal class NotifyExpiringPasswordsOptions +{ + public const string Name = nameof(NotifyExpiringPasswordsBackgroundService); + + public TimeSpan Interval { get; set; } = TimeSpan.FromDays(1); +} \ No newline at end of file diff --git a/Pyro.Api/Pyro/BackgroundServices/RemoveExpiredOneTimePasswords.cs b/Pyro.Api/Pyro/BackgroundServices/RemoveExpiredOneTimePasswords.cs index 3e0fb602..54cfae47 100644 --- a/Pyro.Api/Pyro/BackgroundServices/RemoveExpiredOneTimePasswords.cs +++ b/Pyro.Api/Pyro/BackgroundServices/RemoveExpiredOneTimePasswords.cs @@ -40,6 +40,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (!stoppingToken.IsCancellationRequested) { + // TODO: move to domain var now = timeProvider.GetUtcNow(); var count = await dbContext.Set() .Where(otp => otp.ExpiresAt < now) diff --git a/Pyro.Api/Pyro/DomainEventHandlers/UserCreatedHandler.cs b/Pyro.Api/Pyro/DomainEventHandlers/UserCreatedHandler.cs deleted file mode 100644 index 5b236175..00000000 --- a/Pyro.Api/Pyro/DomainEventHandlers/UserCreatedHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using MediatR; -using Pyro.Domain.Identity.Models; -using Pyro.Domain.UserProfiles; - -namespace Pyro.DomainEventHandlers; - -internal class UserCreatedHandler : INotificationHandler -{ - private readonly IMediator mediator; - - public UserCreatedHandler(IMediator mediator) - => this.mediator = mediator; - - public async Task Handle(UserCreated notification, CancellationToken cancellationToken) - { - // TODO: move handlers to domain projects and create contracts for commands and events? - var command = new CreateUserProfile(notification.UserId, notification.Name); - await mediator.Send(command, cancellationToken); - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro/DtoMappings/DtoMapper.cs b/Pyro.Api/Pyro/DtoMappings/DtoMapper.cs index 7568c7be..a2fd9f3b 100644 --- a/Pyro.Api/Pyro/DtoMappings/DtoMapper.cs +++ b/Pyro.Api/Pyro/DtoMappings/DtoMapper.cs @@ -1,9 +1,6 @@ // Copyright (c) Dmytro Kyshchenko. All rights reserved. // Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. -using Pyro.Contracts.Requests; -using Pyro.Contracts.Responses; -using Pyro.Domain.UserProfiles; using Riok.Mapperly.Abstractions; namespace Pyro.DtoMappings; @@ -11,10 +8,4 @@ namespace Pyro.DtoMappings; [Mapper] public static partial class DtoMapper { - [MapperIgnoreSource(nameof(UserProfile.Id))] - [MapperIgnoreSource(nameof(UserProfile.Avatar))] - [MapperIgnoreSource(nameof(UserProfile.User))] - public static partial UserProfileResponse ToResponse(this UserProfile request); - - public static partial UpdateProfile ToCommand(this UpdateUserProfileRequest request); } \ No newline at end of file diff --git a/Pyro.Api/Pyro/DtoMappings/IdentityMapper.cs b/Pyro.Api/Pyro/DtoMappings/IdentityMapper.cs index 6f425545..e992fdae 100644 --- a/Pyro.Api/Pyro/DtoMappings/IdentityMapper.cs +++ b/Pyro.Api/Pyro/DtoMappings/IdentityMapper.cs @@ -28,10 +28,12 @@ public static partial class IdentityMapper [MapperIgnoreSource(nameof(User.Password))] [MapperIgnoreSource(nameof(User.Salt))] + [MapperIgnoreSource(nameof(User.PasswordExpiresAt))] [MapperIgnoreSource(nameof(User.AuthenticationTokens))] [MapperIgnoreSource(nameof(User.AccessTokens))] [MapperIgnoreSource(nameof(User.OneTimePasswords))] [MapperIgnoreSource(nameof(User.DomainEvents))] + [MapperIgnoreSource(nameof(User.Profile))] public static partial UserResponse ToResponse(this User user); public static partial IReadOnlyList ToResponse(this IReadOnlyList user); @@ -62,4 +64,9 @@ public static partial class IdentityMapper public static partial IEnumerable ToResponse(this IEnumerable token); public static partial GetUsers ToGetUsers(this PageRequest request); + + [MapperIgnoreSource(nameof(UserProfile.Id))] + public static partial UserProfileResponse ToResponse(this UserProfile request); + + public static partial UpdateProfile ToCommand(this UpdateUserProfileRequest request); } \ No newline at end of file diff --git a/Pyro.Api/Pyro/Endpoints/IdentityEndpoints.cs b/Pyro.Api/Pyro/Endpoints/IdentityEndpoints.cs index ce5ff9fa..1753517e 100644 --- a/Pyro.Api/Pyro/Endpoints/IdentityEndpoints.cs +++ b/Pyro.Api/Pyro/Endpoints/IdentityEndpoints.cs @@ -21,7 +21,8 @@ public static IEndpointRouteBuilder MapIdentityEndpoints(this IEndpointRouteBuil .MapUserEndpoints() .MapRoleEndpoints() .MapPermissionEndpoints() - .MapAuthEndpoints(); + .MapAuthEndpoints() + .MapProfileEndpoints(); private static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app) { @@ -383,4 +384,49 @@ private static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder return app; } + + public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuilder app) + { + var profileBuilder = app.MapGroup("/profile") + .WithTags("Profile"); + + profileBuilder.MapGet("/", async ( + IMediator mediator, + CancellationToken cancellationToken) => + { + var request = new GetUserProfile(); + var profile = await mediator.Send(request, cancellationToken); + var result = profile.ToResponse(); + + return Results.Ok(result); + }) + .Produces() + .Produces(401) + .Produces(403) + .ProducesProblem(500) + .WithName("Get Current Profile") + .WithOpenApi(); + + profileBuilder.MapPut("/", async ( + IMediator mediator, + UnitOfWork unitOfWork, + UpdateUserProfileRequest request, + CancellationToken cancellationToken) => + { + var command = request.ToCommand(); + await mediator.Send(command, cancellationToken); + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Results.NoContent(); + }) + .Produces(204) + .ProducesValidationProblem() + .Produces(401) + .Produces(403) + .ProducesProblem(500) + .WithName("Update Current Profile") + .WithOpenApi(); + + return app; + } } \ No newline at end of file diff --git a/Pyro.Api/Pyro/Endpoints/ProfileEndpoints.cs b/Pyro.Api/Pyro/Endpoints/ProfileEndpoints.cs deleted file mode 100644 index ec22a47e..00000000 --- a/Pyro.Api/Pyro/Endpoints/ProfileEndpoints.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Dmytro Kyshchenko. All rights reserved. -// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. - -using MediatR; -using Pyro.Contracts.Requests; -using Pyro.Contracts.Responses; -using Pyro.Domain.UserProfiles; -using Pyro.DtoMappings; -using Pyro.Infrastructure.Shared.DataAccess; - -namespace Pyro.Endpoints; - -internal static class ProfileEndpoints -{ - public static IEndpointRouteBuilder MapProfileEndpoints(this IEndpointRouteBuilder app) - { - var profileBuilder = app.MapGroup("/profile") - .WithTags("Profile"); - - profileBuilder.MapGet("/", async ( - IMediator mediator, - CancellationToken cancellationToken) => - { - var request = new GetUserProfile(); - var profile = await mediator.Send(request, cancellationToken); - var result = profile.ToResponse(); - - return Results.Ok(result); - }) - .Produces() - .Produces(401) - .Produces(403) - .ProducesProblem(500) - .WithName("Get Current Profile") - .WithOpenApi(); - - profileBuilder.MapPut("/", async ( - IMediator mediator, - UnitOfWork unitOfWork, - UpdateUserProfileRequest request, - CancellationToken cancellationToken) => - { - var command = request.ToCommand(); - await mediator.Send(command, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - - return Results.NoContent(); - }) - .Produces(204) - .ProducesValidationProblem() - .Produces(401) - .Produces(403) - .ProducesProblem(500) - .WithName("Update Current Profile") - .WithOpenApi(); - - return app; - } -} \ No newline at end of file diff --git a/Pyro.Api/Pyro/Extensions/LoggingExtensions.cs b/Pyro.Api/Pyro/Extensions/LoggingExtensions.cs new file mode 100644 index 00000000..d82b8540 --- /dev/null +++ b/Pyro.Api/Pyro/Extensions/LoggingExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging.Console; +using Pyro.Services; + +namespace Pyro.Extensions; + +internal static class LoggingExtensions +{ + public static IHostApplicationBuilder AddLogging(this IHostApplicationBuilder builder) + { + builder.Logging.ClearProviders(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder; + } +} \ No newline at end of file diff --git a/Pyro.Api/Pyro/IntegrationEventHandlers/UserCreatedIntegrationEventHandler.cs b/Pyro.Api/Pyro/IntegrationEventHandlers/UserCreatedIntegrationEventHandler.cs index bb6ee705..62e614f4 100644 --- a/Pyro.Api/Pyro/IntegrationEventHandlers/UserCreatedIntegrationEventHandler.cs +++ b/Pyro.Api/Pyro/IntegrationEventHandlers/UserCreatedIntegrationEventHandler.cs @@ -8,8 +8,6 @@ using Pyro.Domain.Identity.Models; using Pyro.Domain.Shared.Email; using Pyro.Domain.Shared.Exceptions; -using Pyro.Domain.UserProfiles; -using Pyro.Infrastructure.Shared.Email; namespace Pyro.IntegrationEventHandlers; @@ -21,7 +19,6 @@ public class UserCreatedIntegrationEventHandler : INotificationHandler logger, @@ -29,8 +26,7 @@ public UserCreatedIntegrationEventHandler( UrlEncoder urlEncoder, IEmailService emailService, IOptions emailServiceOptions, - IUserRepository userRepository, - IUserProfileRepository userProfileRepository) + IUserRepository userRepository) { this.logger = logger; this.urlEncoder = urlEncoder; @@ -38,15 +34,12 @@ public UserCreatedIntegrationEventHandler( this.emailService = emailService; this.emailServiceOptions = emailServiceOptions.Value; this.userRepository = userRepository; - this.userProfileRepository = userProfileRepository; } public async Task Handle(UserCreatedIntegrationEvent notification, CancellationToken cancellationToken) { var user = await userRepository.GetUserById(notification.UserId, cancellationToken) ?? throw new NotFoundException($"The user (Id: {notification.UserId}) was not found."); - var profile = await userProfileRepository.GetUserProfile(notification.UserId, cancellationToken) ?? - throw new NotFoundException($"The user profile (UserId: {notification.UserId}) was not found."); var otp = user.GetRegistrationOneTimePassword() ?? throw new DomainException($"The user (Id: {notification.UserId}) has no registration one-time password."); @@ -64,7 +57,7 @@ Welcome to Pyro! """; var message = new EmailMessage( new EmailAddress("No Reply", $"no-reply@{emailServiceOptions.Domain}"), - new EmailAddress(profile.Name, user.Login), + new EmailAddress(user.Profile.Name, user.Login), "Welcome to Pyro", body); await emailService.SendEmail(message, cancellationToken); diff --git a/Pyro.Api/Pyro/Program.cs b/Pyro.Api/Pyro/Program.cs index f88e72d9..98b81d3c 100644 --- a/Pyro.Api/Pyro/Program.cs +++ b/Pyro.Api/Pyro/Program.cs @@ -23,6 +23,8 @@ var builder = WebApplication.CreateBuilder(args); +builder.AddLogging(); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddPyroProblemDetails(builder.Environment); builder.Services.AddHealthChecks(); @@ -77,7 +79,6 @@ app.MapGroup("/api") .RequireAuthorization() .MapIdentityEndpoints() - .MapProfileEndpoints() .MapGitRepositoryEndpoints() .MapIssueEndpoints(); diff --git a/Pyro.Api/Pyro/ServiceCollectionExtensions.cs b/Pyro.Api/Pyro/ServiceCollectionExtensions.cs index c8f7cf96..da3651a7 100644 --- a/Pyro.Api/Pyro/ServiceCollectionExtensions.cs +++ b/Pyro.Api/Pyro/ServiceCollectionExtensions.cs @@ -27,7 +27,12 @@ public static IHostApplicationBuilder AddPyroInfrastructure(this IHostApplicatio .Configure( configuration .GetRequiredSection("Workers") - .GetRequiredSection(RemoveExpiredOneTimePasswordsOptions.Name)); + .GetRequiredSection(RemoveExpiredOneTimePasswordsOptions.Name)) + .AddHostedService() + .Configure( + configuration + .GetRequiredSection("Workers") + .GetRequiredSection(NotifyExpiringPasswordsOptions.Name)); return builder; } diff --git a/Pyro.Api/Pyro/Services/BasicAuthenticationHandler.cs b/Pyro.Api/Pyro/Services/BasicAuthenticationHandler.cs index c0ec03e0..c1deab36 100644 --- a/Pyro.Api/Pyro/Services/BasicAuthenticationHandler.cs +++ b/Pyro.Api/Pyro/Services/BasicAuthenticationHandler.cs @@ -72,6 +72,13 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail("User is locked"); } + if (user.PasswordExpiresAt < timeProvider.GetUtcNow()) + { + Logger.LogWarning("The password of '{User}' user is expired", user.Login); + + return AuthenticateResult.Fail("Password is expired"); + } + // TODO: implement shared logic with TokenService? var roles = user.Roles.Select(x => x.Name); var permissions = user.Roles.SelectMany(x => x.Permissions).Select(x => x.Name).Distinct(); diff --git a/Pyro.Api/Pyro/Services/GitBackend.cs b/Pyro.Api/Pyro/Services/GitBackend.cs index 9d9d004a..48ea7328 100644 --- a/Pyro.Api/Pyro/Services/GitBackend.cs +++ b/Pyro.Api/Pyro/Services/GitBackend.cs @@ -8,9 +8,9 @@ using System.Text; using Microsoft.Extensions.Options; using Pyro.Domain.GitRepositories; +using Pyro.Domain.Identity; using Pyro.Domain.Shared.CurrentUserProvider; using Pyro.Domain.Shared.Exceptions; -using Pyro.Domain.UserProfiles; using Pyro.Infrastructure; namespace Pyro.Services; @@ -21,7 +21,7 @@ public class GitBackend private readonly IOptions gitOptions; private readonly IGitRepositoryRepository repository; private readonly ICurrentUserProvider currentUserProvider; - private readonly IUserProfileRepository userProfileRepository; + private readonly IUserRepository userRepository; // TODO: move to infrastructure? // TODO: remove HttpContent dependency @@ -30,7 +30,7 @@ public GitBackend( IOptions gitOptions, IGitRepositoryRepository repository, ICurrentUserProvider currentUserProvider, - IUserProfileRepository userProfileRepository) + IUserRepository userRepository) { this.httpContext = httpContextAccessor.HttpContext ?? throw new ArgumentNullException(null, "HttpContext is not available"); @@ -38,7 +38,7 @@ public GitBackend( this.gitOptions = gitOptions; this.repository = repository; this.currentUserProvider = currentUserProvider; - this.userProfileRepository = userProfileRepository; + this.userRepository = userRepository; } public async Task Handle(string repositoryName, CancellationToken cancellationToken = default) @@ -47,8 +47,8 @@ public async Task Handle(string repositoryName, CancellationToken cancellationTo throw new NotFoundException("Repository not found"); var currentUser = currentUserProvider.GetCurrentUser(); - var userProfile = await userProfileRepository.GetUserProfile(currentUser.Id, cancellationToken) ?? - throw new NotFoundException("User profile not found"); + var user = await userRepository.GetUserById(currentUser.Id, cancellationToken) ?? + throw new NotFoundException($"User '{currentUser.Id}' not found"); var basePath = gitOptions.Value.BasePath; var gitPath = Path.Combine(basePath, $"{gitRepository.Name}.git"); @@ -77,8 +77,8 @@ public async Task Handle(string repositoryName, CancellationToken cancellationTo { "HTTP_CONTENT_ENCODING", httpContext.Request.Headers.ContentEncoding }, { "REMOTE_USER", currentUser.Login }, { "REMOTE_ADDR", httpContext.Connection.RemoteIpAddress?.ToString() }, - { "GIT_COMMITTER_NAME", currentUser.Login }, - { "GIT_COMMITTER_EMAIL", userProfile.User.Email }, + { "GIT_COMMITTER_NAME", user.Profile.Name }, + { "GIT_COMMITTER_EMAIL", user.Login }, }, }; diff --git a/Pyro.Api/Pyro/Services/LoggerProvider.cs b/Pyro.Api/Pyro/Services/LoggerProvider.cs new file mode 100644 index 00000000..80b2ad9a --- /dev/null +++ b/Pyro.Api/Pyro/Services/LoggerProvider.cs @@ -0,0 +1,55 @@ +// Copyright (c) Dmytro Kyshchenko. All rights reserved. +// Licensed under the GPL-3.0 license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging.Console; + +namespace Pyro.Services; + +internal sealed class LoggerProvider : ILoggerProvider +{ + private readonly ConsoleLoggerProvider loggerProvider; + + public LoggerProvider(ConsoleLoggerProvider loggerProvider) + => this.loggerProvider = loggerProvider; + + public void Dispose() + => loggerProvider.Dispose(); + + public ILogger CreateLogger(string categoryName) + { + var logger = loggerProvider.CreateLogger(categoryName); + + return new Logger(logger); + } +} + +internal sealed class Logger : ILogger +{ + private readonly ILogger logger; + + public Logger(ILogger logger) + => this.logger = logger; + + public IDisposable? BeginScope(TState state) where TState : notnull + => logger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) + => logger.IsEnabled(logLevel); + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var log = formatter(state, exception); + if (log.Contains("-- NO_LOGGING")) + return; + + logger.Log(logLevel, eventId, state, exception, formatter); + } +} \ No newline at end of file diff --git a/Pyro.Api/Pyro/appsettings.json b/Pyro.Api/Pyro/appsettings.json index b80b8aa2..ec6584cf 100644 --- a/Pyro.Api/Pyro/appsettings.json +++ b/Pyro.Api/Pyro/appsettings.json @@ -30,6 +30,9 @@ }, "RemoveExpiredOneTimePasswords": { "Interval": "00:30:00" + }, + "NotifyExpiringPasswordsBackgroundService": { + "Interval": "1.00:00:00" } } } \ No newline at end of file diff --git a/Pyro.UI/src/app/components/settings/profile-edit/profile-edit.component.html b/Pyro.UI/src/app/components/settings/profile-edit/profile-edit.component.html index f0c5f661..2a86bec4 100644 --- a/Pyro.UI/src/app/components/settings/profile-edit/profile-edit.component.html +++ b/Pyro.UI/src/app/components/settings/profile-edit/profile-edit.component.html @@ -3,17 +3,6 @@ class="w-full" [formGroup]="form" (ngSubmit)="onSubmit()"> -
- - - -
-
{ this.form.patchValue({ - email: profile?.email, name: profile?.name, status: profile?.status, }); diff --git a/Pyro.UI/src/app/services/profile.service.ts b/Pyro.UI/src/app/services/profile.service.ts index 3f6cd1f4..b794ad5e 100644 --- a/Pyro.UI/src/app/services/profile.service.ts +++ b/Pyro.UI/src/app/services/profile.service.ts @@ -24,7 +24,6 @@ export class ProfileService { } export interface Profile { - get email(): string; get name(): string | null; get status(): string | null; }