From 1d3de42b0d7feea8e8e97bf312ecebf4d3af0155 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Fri, 26 May 2023 10:07:12 +0700 Subject: [PATCH 001/162] feat: add volume to docker compose file (#99) --- docker-compose.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f478e29b..4ba0a076 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,4 +20,9 @@ services: - POSTGRES_PASSWORD=supasecured - POSTGRES_DB=mydb ports: - - '5432:5432' \ No newline at end of file + - '5432:5432' + volumes: + - pg_data:/var/lib/postgresql/data + +volumes: + pg_data: \ No newline at end of file From fea43813da0b08277b4fa15295c00d0495c3695f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 26 May 2023 12:14:58 +0700 Subject: [PATCH 002/162] Cross cutting/auth (#100) * feat: auth with refresh token and jwe using cookies scheme * feat: log out * add: data seed * add: validate endpoint * refactor: change implementation to removing cookies * fix: disable command not handle all cases and refactor something * add: seed env * add: allow credentials * update: origin localhost 3000 on dev * fix: revert to any origin --- src/Api/ConfigureServices.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 9420dab1..0fd0aa2b 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -23,6 +23,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services builder.AllowAnyOrigin(); builder.AllowAnyHeader(); builder.AllowAnyMethod(); + builder.AllowCredentials(); }); }); From ffc92cdb154ef2bb57c760e8be1d146d87eb9bdb Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 26 May 2023 12:19:33 +0700 Subject: [PATCH 003/162] Cross cutting/auth (#101) * feat: auth with refresh token and jwe using cookies scheme * feat: log out * add: data seed * add: validate endpoint * refactor: change implementation to removing cookies * fix: disable command not handle all cases and refactor something * add: seed env * add: allow credentials * update: origin localhost 3000 on dev * fix: revert to any origin * fix: revert to local 3000 --- src/Api/ConfigureServices.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 0fd0aa2b..6dea0c7d 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -20,7 +20,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services { options.AddPolicy("AllowAllOrigins", builder => { - builder.AllowAnyOrigin(); + builder.WithOrigins("http://localhost:3000"); builder.AllowAnyHeader(); builder.AllowAnyMethod(); builder.AllowCredentials(); From 5c6367aea9d6685917ec2faf01814386fab60b75 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Fri, 26 May 2023 14:33:52 +0700 Subject: [PATCH 004/162] add: expiry date of jwe to be the same with refresh tokne --- src/Api/Controllers/AuthController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index b0941458..fd9a8ac3 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -31,7 +31,7 @@ public async Task>> Login([FromBody] LoginModel var result = await _identityService.LoginAsync(loginModel.Email, loginModel.Password); SetRefreshToken(result.AuthResult.RefreshToken); - SetJweToken(result.AuthResult.Token); + SetJweToken(result.AuthResult.Token, result.AuthResult.RefreshToken); var loginResult = new LoginResult() { @@ -75,7 +75,7 @@ public async Task Refresh() var authResult = await _identityService.RefreshTokenAsync(jweToken!, refreshToken!); SetRefreshToken(authResult.RefreshToken); - SetJweToken(authResult.Token); + SetJweToken(authResult.Token, authResult.RefreshToken); return Ok(); } @@ -97,11 +97,12 @@ public async Task Validate() return Unauthorized(); } - private void SetJweToken(SecurityToken jweToken) + private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken) { var cookieOptions = new CookieOptions { HttpOnly = true, + Expires = newRefreshToken.ExpiryDateTime }; var handler = new JwtSecurityTokenHandler(); Response.Cookies.Append("JweToken", handler.WriteToken(jweToken), cookieOptions); From 4f8a653b3e4a805ca2aeb56afaf17e0d9627c1af Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 26 May 2023 15:30:03 +0700 Subject: [PATCH 005/162] Cross cutting/auth (#102) * feat: auth with refresh token and jwe using cookies scheme * feat: log out * add: data seed * add: validate endpoint * refactor: change implementation to removing cookies * fix: disable command not handle all cases and refactor something * add: seed env * add: allow credentials * update: origin localhost 3000 on dev * fix: revert to any origin * fix: revert to local 3000 * fix: refresh endpoint no need to be authorized --- src/Api/Controllers/AuthController.cs | 17 +++-------------- src/Api/appsettings.Development.json | 2 +- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index fd9a8ac3..da4bef58 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -65,7 +65,6 @@ public async Task Logout() return Ok(); } - [Authorize] [HttpPost] public async Task Refresh() { @@ -82,21 +81,11 @@ public async Task Refresh() [Authorize] [HttpPost] - public async Task Validate() + public IActionResult Validate() { - var refreshToken = Request.Cookies[nameof(RefreshToken)]; - var jweToken = Request.Cookies["JweToken"]; - - var validated = await _identityService.Validate(jweToken!, refreshToken!); - - if (validated) - { - return Ok(); - } - - return Unauthorized(); + return Ok(); } - + private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken) { var cookieOptions = new CookieOptions diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 41b82277..535b16ec 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -6,7 +6,7 @@ "JweSettings": { "SigningKeyId": "4bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b5014", "EncryptionKeyId": "4bd28be8eac5414fb01c5cbe343b5014", - "TokenLifetime": "00:20:00", + "TokenLifetime": "00:00:05", "RefreshTokenLifetimeInDays": 3 }, "Seed": true, From 98a5cb68ff4dd12bad7187665857a3d2386c6d78 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 26 May 2023 16:15:37 +0700 Subject: [PATCH 006/162] Cross cutting/auth (#103) * feat: auth with refresh token and jwe using cookies scheme * feat: log out * add: data seed * add: validate endpoint * refactor: change implementation to removing cookies * fix: disable command not handle all cases and refactor something * add: seed env * add: allow credentials * update: origin localhost 3000 on dev * fix: revert to any origin * fix: revert to local 3000 * fix: refresh endpoint no need to be authorized * update: authentication exception now return 400 instead of 401 --- src/Api/Middlewares/ExceptionMiddleware.cs | 2 +- src/Api/appsettings.Development.json | 2 +- src/Infrastructure/Identity/IdentityService.cs | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index 194d73ab..cb6eb9e0 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -94,7 +94,7 @@ private async void HandleLimitExceededException(HttpContext context, Exception e private async void HandleAuthenticationException(HttpContext context, Exception ex) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.StatusCode = StatusCodes.Status400BadRequest; await WriteExceptionMessageAsync(context, ex); } diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 535b16ec..41b82277 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -6,7 +6,7 @@ "JweSettings": { "SigningKeyId": "4bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b5014", "EncryptionKeyId": "4bd28be8eac5414fb01c5cbe343b5014", - "TokenLifetime": "00:00:05", + "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, "Seed": true, diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index d1b36fac..58b7cf6e 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -119,17 +119,6 @@ public async Task RefreshTokenAsync(string token, string r { throw new AuthenticationException("Invalid token."); } - - var expiryDateUnix = - long.Parse(validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Exp)).Value); - - var expiryDateTimeUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) - .AddSeconds(expiryDateUnix); - - if (expiryDateTimeUtc > DateTime.UtcNow) - { - throw new AuthenticationException("This token has not expired yet."); - } var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); From 16e8a0b10082db84b66ff6cea1f434e727422657 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 26 May 2023 21:51:51 +0700 Subject: [PATCH 007/162] update: refresh and login endpoint now delete db refresh tokens and refresh tokens are now persistent instead of fleeting (#104) --- .../Identity/IdentityService.cs | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 58b7cf6e..cb38b55c 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -148,10 +148,19 @@ public async Task RefreshTokenAsync(string token, string r throw new AuthenticationException("This refresh token does not match this Jwt."); } - _context.RefreshTokens.Remove(storedRefreshToken); + var jweToken = CreateJweToken(user); + + storedRefreshToken.JwtId = jweToken.Id; + storedRefreshToken.ExpiryDateTime = + LocalDateTime.FromDateTime(DateTime.UtcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)); + _context.RefreshTokens.Update(storedRefreshToken); await _context.SaveChangesAsync(); - return await GenerateAuthenticationResultForUserAsync(user); + return new AuthenticationResult() + { + Token = jweToken, + RefreshToken = _mapper.Map(storedRefreshToken) + }; } private ClaimsPrincipal? GetPrincipalFromToken(string token) @@ -196,6 +205,11 @@ public async Task RefreshTokenAsync(string token, string r throw new AuthenticationException("Username or password is invalid."); } + var existedRefreshTokens = _context.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); + + _context.RemoveRange(existedRefreshTokens); + await _context.SaveChangesAsync(); + return (await GenerateAuthenticationResultForUserAsync(user), _mapper.Map(user)); } @@ -226,6 +240,28 @@ public async Task LogoutAsync(string token, string refreshToken) } private async Task GenerateAuthenticationResultForUserAsync(User user) + { + var token = CreateJweToken(user); + var utcNow = DateTime.UtcNow; + + var refreshToken = new RefreshToken() + { + JwtId = token.Id, + User = user, + CreationDateTime = LocalDateTime.FromDateTime(utcNow), + ExpiryDateTime = LocalDateTime.FromDateTime(utcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)) + }; + + await _context.RefreshTokens.AddAsync(refreshToken); + await _context.SaveChangesAsync(); + return new() + { + Token = token, + RefreshToken = _mapper.Map(refreshToken) + }; + } + + private SecurityToken CreateJweToken(User user) { var utcNow = DateTime.UtcNow; var authClaims = new List @@ -252,20 +288,6 @@ private async Task GenerateAuthenticationResultForUserAsyn var token = handler.CreateToken(tokenDescriptor); - var refreshToken = new RefreshToken() - { - JwtId = token.Id, - User = user, - CreationDateTime = LocalDateTime.FromDateTime(utcNow), - ExpiryDateTime = LocalDateTime.FromDateTime(utcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)) - }; - - await _context.RefreshTokens.AddAsync(refreshToken); - await _context.SaveChangesAsync(); - return new() - { - Token = token, - RefreshToken = _mapper.Map(refreshToken) - }; + return token; } } \ No newline at end of file From 5a9877b9dca2ba2510e8970814e2ece78b70f57a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 27 May 2023 18:31:22 +0700 Subject: [PATCH 008/162] Fix/logout (#106) * fix: logout not working * fix: so is refresh * fix: something --- src/Api/Controllers/AuthController.cs | 7 ++----- .../Common/Interfaces/IIdentityService.cs | 2 +- src/Infrastructure/Identity/IdentityService.cs | 14 ++++---------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index da4bef58..b8d8a03c 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -48,19 +48,16 @@ public async Task>> Login([FromBody] LoginModel return Ok(Result.Succeed(loginResult)); } - [Authorize] [HttpPost] public async Task Logout() { var refreshToken = Request.Cookies[nameof(RefreshToken)]; var jweToken = Request.Cookies["JweToken"]; - - var loggedOut = await _identityService.LogoutAsync(jweToken!, refreshToken!); - - if (!loggedOut) return Ok(); RemoveJweToken(); RemoveRefreshToken(); + + await _identityService.LogoutAsync(jweToken!, refreshToken!); return Ok(); } diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 20fab955..048300af 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -8,5 +8,5 @@ public interface IIdentityService Task Validate(string token, string refreshToken); Task RefreshTokenAsync(string token, string refreshToken); Task<(AuthenticationResult AuthResult, UserDto UserCredentials)> LoginAsync(string email, string password); - Task LogoutAsync(string token, string refreshToken); + Task LogoutAsync(string token, string refreshToken); } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index cb38b55c..eff63f43 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Security.Authentication; @@ -185,15 +186,10 @@ public async Task RefreshTokenAsync(string token, string r return principal; } - catch (SecurityTokenExpiredException ex) + catch { return null; } - catch (Exception exception) - { - Console.WriteLine(exception.StackTrace); - return null; - } } public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) @@ -213,7 +209,7 @@ public async Task RefreshTokenAsync(string token, string r return (await GenerateAuthenticationResultForUserAsync(user), _mapper.Map(user)); } - public async Task LogoutAsync(string token, string refreshToken) + public async Task LogoutAsync(string token, string refreshToken) { var validatedToken = GetPrincipalFromToken(token); @@ -226,7 +222,7 @@ public async Task LogoutAsync(string token, string refreshToken) var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); - if (storedRefreshToken is null) return true; + if (storedRefreshToken is null) return; if (!storedRefreshToken!.JwtId.Equals(jti)) { @@ -235,8 +231,6 @@ public async Task LogoutAsync(string token, string refreshToken) _context.RefreshTokens.Remove(storedRefreshToken); await _context.SaveChangesAsync(); - - return true; } private async Task GenerateAuthenticationResultForUserAsync(User user) From 11ecf1697df6adc1adeb35d07b497ea34defca1a Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 27 May 2023 18:55:11 +0700 Subject: [PATCH 009/162] Cuong/refactor (#105) * refactor(ExceptionMiddleware.cs): add static modifier to handle methods * refactor(AuthController.cs): add ProduceResponseType to endpoints * refactor(Departments): error message * refactor(controller): Auth, Departments, Documents, Folders, Locker * refactor most api endpoints --- src/Api/Controllers/AuthController.cs | 6 +++ src/Api/Controllers/DepartmentsController.cs | 4 +- src/Api/Controllers/DocumentsController.cs | 8 +-- src/Api/Controllers/FoldersController.cs | 10 +++- src/Api/Controllers/LockersController.cs | 13 +++++ src/Api/Controllers/RoomsController.cs | 18 ++++--- src/Api/Controllers/StaffsController.cs | 4 +- src/Api/Controllers/UsersController.cs | 11 ++-- src/Api/Middlewares/ExceptionMiddleware.cs | 14 ++--- .../AddDepartmentCommand.cs} | 16 +++--- .../ImportDocument/ImportDocumentCommand.cs | 12 ++--- .../Commands/AddFolder/AddFolderCommand.cs | 5 +- .../DisableFolder/DisableFolderCommand.cs | 3 +- .../Commands/AddLocker/AddLockerCommand.cs | 10 ++-- .../Rooms/Commands/AddRoom/AddRoomCommand.cs | 51 +++++++++++++++++++ .../AddRoomCommandValidator.cs} | 6 +-- .../Commands/CreateRoom/CreateRoomCommand.cs | 41 --------------- .../DisableRoom/DisableRoomCommand.cs | 2 +- .../GetEmptyContainersPaginatedQuery.cs | 2 +- .../AddStaffCommand.cs} | 14 ++--- .../AddUserCommand.cs} | 32 ++++++------ .../AddUserCommandValidator.cs} | 19 ++++--- .../DisableUser/DisableUserCommand.cs | 3 +- .../BaseClassFixture.cs | 4 +- .../CustomApiFactory.cs | 2 +- ...partmentTests.cs => AddDepartmentTests.cs} | 6 +-- .../Folders/Commands/AddFolderTests.cs | 4 +- .../Folders/Commands/DisableFolderTests.cs | 3 +- .../Lockers/Commands/AddLockerTests.cs | 2 +- .../Rooms/Commands/DisableRoomTests.cs | 2 +- .../{CreateUserTests.cs => AddUserTests.cs} | 8 +-- 31 files changed, 190 insertions(+), 145 deletions(-) rename src/Application/Departments/Commands/{CreateDepartment/CreateDepartmentCommand.cs => AddDepartment/AddDepartmentCommand.cs} (60%) create mode 100644 src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs rename src/Application/Rooms/Commands/{CreateRoom/CreateRoomCommandValidator.cs => AddRoom/AddRoomCommandValidator.cs} (82%) delete mode 100644 src/Application/Rooms/Commands/CreateRoom/CreateRoomCommand.cs rename src/Application/Staffs/Commands/{CreateStaff/CreateStaffCommand.cs => AddStaff/AddStaffCommand.cs} (67%) rename src/Application/Users/Commands/{CreateUser/CreateUserCommand.cs => AddUser/AddUserCommand.cs} (68%) rename src/Application/Users/Commands/{CreateUser/CreateUserCommandValidator.cs => AddUser/AddUserCommandValidator.cs} (67%) rename tests/Application.Tests.Integration/Departments/Commands/{CreateDepartmentTests.cs => AddDepartmentTests.cs} (88%) rename tests/Application.Tests.Integration/Users/Commands/{CreateUserTests.cs => AddUserTests.cs} (87%) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index b8d8a03c..dac02af4 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -49,6 +49,8 @@ public async Task>> Login([FromBody] LoginModel } [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Logout() { var refreshToken = Request.Cookies[nameof(RefreshToken)]; @@ -63,6 +65,8 @@ public async Task Logout() } [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Refresh() { var refreshToken = Request.Cookies[nameof(RefreshToken)]; @@ -78,6 +82,8 @@ public async Task Refresh() [Authorize] [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public IActionResult Validate() { return Ok(); diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 6a010c00..465738a9 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -1,5 +1,5 @@ using Application.Common.Models; -using Application.Departments.Commands.CreateDepartment; +using Application.Departments.Commands.AddDepartment; using Application.Departments.Queries.GetAllDepartments; using Application.Identity; using Application.Users.Queries; @@ -20,7 +20,7 @@ public class DepartmentsController : ApiControllerBase [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> CreateDepartment([FromBody] CreateDepartmentCommand command) + public async Task>> AddDepartment([FromBody] AddDepartmentCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 7d491b2b..a4b4d8bf 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,12 +1,10 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; -using Application.Departments.Commands.CreateDepartment; using Application.Documents.Commands.ImportDocument; using Application.Documents.Queries.GetAllDocumentsPaginated; using Application.Documents.Queries.GetDocumentById; using Application.Documents.Queries.GetDocumentTypes; using Application.Identity; -using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,13 +12,13 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] public async Task>> ImportDocument([FromBody] ImportDocumentCommand command) { var result = await Mediator.Send(command); @@ -31,7 +29,6 @@ public async Task>> ImportDocument([FromBody] I [HttpGet("types")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllDocumentTypes() { var result = await Mediator.Send(new GetAllDocumentTypesQuery()); @@ -61,6 +58,9 @@ public async Task>>> GetAllDocume } [HttpGet("{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetDocumentById(Guid id) { var query = new GetDocumentByIdQuery() diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 77664c44..e37a6691 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -10,9 +10,9 @@ namespace Api.Controllers; public class FoldersController : ApiControllerBase { - [RequiresRole(IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -23,7 +23,13 @@ public async Task>> AddFolder([FromBody] AddFolde return Ok(Result.Succeed(result)); } + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("disable")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> DisableFolder([FromBody] DisableFolderCommand command) { var result = await Mediator.Send(command); diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 1dd1b3ce..350ca406 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -14,6 +14,7 @@ public class LockersController : ApiControllerBase [RequiresRole(IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] @@ -23,14 +24,26 @@ public async Task>> AddLocker([FromBody] AddLocke return Ok(Result.Succeed(result)); } + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("disable")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> DisableLocker([FromBody] DisableLockerCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("enable")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> EnableLocker([FromBody] EnableLockerCommand command) { var result = await Mediator.Send(command); diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 8d8916d6..39c156e9 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,7 +1,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; -using Application.Rooms.Commands.CreateRoom; +using Application.Rooms.Commands.AddRoom; using Application.Rooms.Commands.DisableRoom; using Application.Rooms.Commands.RemoveRoom; using Application.Rooms.Queries.GetEmptyContainersPaginated; @@ -15,9 +15,11 @@ public class RoomsController : ApiControllerBase [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> AddRoom(CreateRoomCommand command) + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> AddRoom(AddRoomCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -34,20 +36,24 @@ public async Task>> GetEmptyContainer return Ok(Result>.Succeed(result)); } - [HttpPut] + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPut("disable")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> DisableRoom(DisableRoomCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + [RequiresRole(IdentityData.Roles.Admin)] [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveRoom(RemoveRoomCommand command) { var result = await Mediator.Send(command); diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 255335ef..d714b2bf 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -1,6 +1,6 @@ using Application.Common.Models; using Application.Identity; -using Application.Staffs.Commands.CreateStaff; +using Application.Staffs.Commands.AddStaff; using Application.Users.Queries.Physical; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,7 +14,7 @@ public class StaffsController : ApiControllerBase [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> CreateStaff([FromBody] CreateStaffCommand command) + public async Task>> AddStaff([FromBody] AddStaffCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 33aa1714..1d51f7f0 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,6 +1,6 @@ using Application.Common.Models; using Application.Identity; -using Application.Users.Commands.CreateUser; +using Application.Users.Commands.AddUser; using Application.Users.Commands.DisableUser; using Application.Users.Queries; using Application.Users.Queries.GetUsersByName; @@ -12,19 +12,19 @@ namespace Api.Controllers; public class UsersController : ApiControllerBase { + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> CreateUser([FromBody] CreateUserCommand command) + public async Task>> AddUser([FromBody] AddUserCommand command) { var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - [Authorize] + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] @@ -41,11 +41,12 @@ public async Task>>> GetUsersByName(s return Ok(Result>.Succeed(result)); } - [HttpPost("disable")] [RequiresRole(IdentityData.Roles.Admin)] + [HttpPost("disable")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> DisableUser([FromBody] DisableUserCommand command) { var result = await Mediator.Send(command); diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index cb6eb9e0..c08e16ed 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -54,25 +54,25 @@ private async Task HandleExceptionAsync(HttpContext context, Exception ex) } - private async void HandleKeyNotFoundException(HttpContext context, Exception ex) + private static async void HandleKeyNotFoundException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status404NotFound; await WriteExceptionMessageAsync(context, ex); } - private async void HandleConflictException(HttpContext context, Exception ex) + private static async void HandleConflictException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status409Conflict; await WriteExceptionMessageAsync(context, ex); } - private async void HandleNotAllowedException(HttpContext context, Exception ex) + private static async void HandleNotAllowedException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status406NotAcceptable; await WriteExceptionMessageAsync(context, ex); } - private async void HandleRequestValidationException(HttpContext context, Exception ex) + private static async void HandleRequestValidationException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status400BadRequest; @@ -86,13 +86,13 @@ private async void HandleRequestValidationException(HttpContext context, Excepti await context.Response.Body.WriteAsync(SerializeToUtf8BytesWeb(result)); } - private async void HandleLimitExceededException(HttpContext context, Exception ex) + private static async void HandleLimitExceededException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status409Conflict; await WriteExceptionMessageAsync(context, ex); } - private async void HandleAuthenticationException(HttpContext context, Exception ex) + private static async void HandleAuthenticationException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await WriteExceptionMessageAsync(context, ex); @@ -104,7 +104,7 @@ private static async void HandleUnauthorizedAccessException(HttpContext context, await WriteExceptionMessageAsync(context, ex); } - private async void HandleInvalidOperationException(HttpContext context, Exception ex) + private static async void HandleInvalidOperationException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status409Conflict; await WriteExceptionMessageAsync(context, ex); diff --git a/src/Application/Departments/Commands/CreateDepartment/CreateDepartmentCommand.cs b/src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs similarity index 60% rename from src/Application/Departments/Commands/CreateDepartment/CreateDepartmentCommand.cs rename to src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs index cda6a16d..00f42e5c 100644 --- a/src/Application/Departments/Commands/CreateDepartment/CreateDepartmentCommand.cs +++ b/src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs @@ -6,30 +6,30 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace Application.Departments.Commands.CreateDepartment; +namespace Application.Departments.Commands.AddDepartment; -public record CreateDepartmentCommand : IRequest +public record AddDepartmentCommand : IRequest { - public string Name { get; init; } + public string Name { get; init; } = null!; } -public class CreateDepartmentCommandHandler : IRequestHandler +public class AddDepartmentCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CreateDepartmentCommandHandler(IApplicationDbContext context, IMapper mapper) + public AddDepartmentCommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } - public async Task Handle(CreateDepartmentCommand request, CancellationToken cancellationToken) + public async Task Handle(AddDepartmentCommand request, CancellationToken cancellationToken) { - var department = await _context.Departments.FirstOrDefaultAsync(x => x.Name.Equals(request.Name), cancellationToken); + var department = await _context.Departments.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); if (department is not null) { - throw new ConflictException("Department name already exists"); + throw new ConflictException("Department name already exists."); } var entity = new Department { diff --git a/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs b/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs index 373e7202..7989bf5a 100644 --- a/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs +++ b/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs @@ -10,9 +10,9 @@ namespace Application.Documents.Commands.ImportDocument; public record ImportDocumentCommand : IRequest { - public string Title { get; init; } + public string Title { get; init; } = null!; public string? Description { get; init; } - public string DocumentType { get; init; } + public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } public Guid FolderId { get; init; } } @@ -35,23 +35,23 @@ public async Task Handle(ImportDocumentCommand request, Cancellatio .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); if (importer is null) { - throw new KeyNotFoundException("User does not exist"); + throw new KeyNotFoundException("User does not exist."); } var document = _context.Documents.FirstOrDefault(x => - x.Title.Equals(request.Title) + x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) && x.Importer != null && x.Importer.Id == request.ImporterId); if (document is not null) { - throw new ConflictException($"Document title already exists for user {importer.LastName}"); + throw new ConflictException($"Document title already exists for user {importer.LastName}."); } var folder = await _context.Folders .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); if (folder is null) { - throw new KeyNotFoundException("Folder does not exist"); + throw new KeyNotFoundException("Folder does not exist."); } var entity = new Document() diff --git a/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs b/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs index e3fd551d..bf974013 100644 --- a/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs +++ b/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs @@ -11,7 +11,7 @@ namespace Application.Folders.Commands.AddFolder; public record AddFolderCommand : IRequest { - public string Name { get; init; } + public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } public Guid LockerId { get; init; } @@ -42,7 +42,8 @@ public async Task Handle(AddFolderCommand request, CancellationToken throw new LimitExceededException("This locker cannot accept more folders."); } - var folder = await _context.Folders.FirstOrDefaultAsync(x => x.Name.Trim().Equals(request.Name.Trim()) && x.Locker.Id.Equals(request.LockerId), cancellationToken); + var folder = await _context.Folders.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) + && x.Locker.Id.Equals(request.LockerId), cancellationToken); if (folder is not null) { diff --git a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs b/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs index d49909a6..adf845df 100644 --- a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs +++ b/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -34,7 +35,7 @@ public async Task Handle(DisableFolderCommand request, CancellationTo if (!folder.IsAvailable) { - throw new InvalidOperationException("Folder has already been disabled."); + throw new ConflictException("Folder has already been disabled."); } if (folder.NumberOfDocuments > 0) diff --git a/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs b/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs index f23cc079..7a47bd87 100644 --- a/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs +++ b/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs @@ -11,8 +11,8 @@ namespace Application.Lockers.Commands.AddLocker; public record AddLockerCommand : IRequest { - public string Name { get; init; } - public string Description { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } public Guid RoomId { get; init; } public int Capacity { get; init; } } @@ -44,16 +44,16 @@ public async Task Handle(AddLockerCommand request, CancellationToken ); } - var locker = await _context.Lockers.FirstOrDefaultAsync(x => x.Name.Trim().Equals(request.Name.Trim()) && x.Room.Id.Equals(request.RoomId)); + var locker = await _context.Lockers.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Room.Id.Equals(request.RoomId) ,cancellationToken); if (locker is not null) { - throw new ConflictException("Locker's name already exists."); + throw new ConflictException("Locker name already exists."); } var entity = new Locker { Name = request.Name.Trim(), - Description = request.Description, + Description = request.Description?.Trim(), NumberOfFolders = 0, Capacity = request.Capacity, Room = room, diff --git a/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs b/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs new file mode 100644 index 00000000..f255b988 --- /dev/null +++ b/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs @@ -0,0 +1,51 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Commands.AddRoom; + +public record AddRoomCommand : IRequest +{ + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + +} + +public class AddRoomCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public AddRoomCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(AddRoomCommand request, CancellationToken cancellationToken) + { + + var room = await _context.Rooms.FirstOrDefaultAsync(r => + r.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); + + if (room is not null) + { + throw new ConflictException("Room name already exists."); + } + + var entity = new Room + { + Name = request.Name.Trim(), + Description = request.Description?.Trim(), + NumberOfLockers = 0, + Capacity = request.Capacity + }; + var result = await _context.Rooms.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/CreateRoom/CreateRoomCommandValidator.cs b/src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs similarity index 82% rename from src/Application/Rooms/Commands/CreateRoom/CreateRoomCommandValidator.cs rename to src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs index 14bfc7ca..bfa65d1a 100644 --- a/src/Application/Rooms/Commands/CreateRoom/CreateRoomCommandValidator.cs +++ b/src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs @@ -1,12 +1,12 @@ using Application.Common.Interfaces; using FluentValidation; -namespace Application.Rooms.Commands.CreateRoom; +namespace Application.Rooms.Commands.AddRoom; -public class CreateRoomCommandValidator : AbstractValidator +public class AddRoomCommandValidator : AbstractValidator { private readonly IApplicationDbContext _context; - public CreateRoomCommandValidator(IApplicationDbContext context) + public AddRoomCommandValidator(IApplicationDbContext context) { _context = context; diff --git a/src/Application/Rooms/Commands/CreateRoom/CreateRoomCommand.cs b/src/Application/Rooms/Commands/CreateRoom/CreateRoomCommand.cs deleted file mode 100644 index 53c24c4d..00000000 --- a/src/Application/Rooms/Commands/CreateRoom/CreateRoomCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Physical; -using MediatR; - -namespace Application.Rooms.Commands.CreateRoom; - -public record CreateRoomCommand : IRequest -{ - public string Name { get; init; } - public string Description { get; init; } - public int Capacity { get; init; } - -} - -public class CreateRoomCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public CreateRoomCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(CreateRoomCommand request, CancellationToken cancellationToken) - { - var entity = new Room - { - Name = request.Name, - Description = request.Description, - NumberOfLockers = 0, - Capacity = request.Capacity - }; - var result = await _context.Rooms.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs b/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs index dbf28e54..91ca9bdb 100644 --- a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs +++ b/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs @@ -37,7 +37,7 @@ public async Task Handle(DisableRoomCommand request, CancellationToken if (!room.IsAvailable) { - throw new InvalidOperationException("Room have already been disabled."); + throw new ConflictException("Room have already been disabled."); } var canNotDisable = await _context.Documents diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs b/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs index dcd6cbf5..166881aa 100644 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs +++ b/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs @@ -29,7 +29,7 @@ public async Task> Handle(GetEmptyContainersPagina var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) { - throw new KeyNotFoundException("Room does not exist"); + throw new KeyNotFoundException("Room does not exist."); } var lockers = _context.Lockers diff --git a/src/Application/Staffs/Commands/CreateStaff/CreateStaffCommand.cs b/src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs similarity index 67% rename from src/Application/Staffs/Commands/CreateStaff/CreateStaffCommand.cs rename to src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs index bf5fbfa1..409e8cf9 100644 --- a/src/Application/Staffs/Commands/CreateStaff/CreateStaffCommand.cs +++ b/src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs @@ -5,37 +5,37 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace Application.Staffs.Commands.CreateStaff; +namespace Application.Staffs.Commands.AddStaff; -public record CreateStaffCommand : IRequest +public record AddStaffCommand : IRequest { public Guid UserId { get; init; } public Guid RoomId { get; init; } } -public class CreateStaffCommandHandler : IRequestHandler +public class AddStaffCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CreateStaffCommandHandler(IApplicationDbContext context, IMapper mapper) + public AddStaffCommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } - public async Task Handle(CreateStaffCommand request, CancellationToken cancellationToken) + public async Task Handle(AddStaffCommand request, CancellationToken cancellationToken) { var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); if (user is null) { - throw new KeyNotFoundException("User does not exist"); + throw new KeyNotFoundException("User does not exist."); } var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) { - throw new KeyNotFoundException("Room does not exist"); + throw new KeyNotFoundException("Room does not exist."); } var staff = new Staff diff --git a/src/Application/Users/Commands/CreateUser/CreateUserCommand.cs b/src/Application/Users/Commands/AddUser/AddUserCommand.cs similarity index 68% rename from src/Application/Users/Commands/CreateUser/CreateUserCommand.cs rename to src/Application/Users/Commands/AddUser/AddUserCommand.cs index c0ed1617..5f5f684c 100644 --- a/src/Application/Users/Commands/CreateUser/CreateUserCommand.cs +++ b/src/Application/Users/Commands/AddUser/AddUserCommand.cs @@ -9,37 +9,37 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Users.Commands.CreateUser; +namespace Application.Users.Commands.AddUser; -public record CreateUserCommand : IRequest +public record AddUserCommand : IRequest { - public string Username { get; init; } - public string Email { get; init; } - public string Password { get; init; } - public string FirstName { get; init; } - public string LastName { get; init; } + public string Username { get; init; } = null!; + public string Email { get; init; } = null!; + public string Password { get; init; } = null!; + public string? FirstName { get; init; } + public string? LastName { get; init; } public Guid DepartmentId { get; init; } - public string Role { get; init; } - public string Position { get; init; } + public string Role { get; init; } = null!; + public string? Position { get; init; } } -public class CreateUserCommandHandler : IRequestHandler +public class AddUserCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CreateUserCommandHandler(IApplicationDbContext context, IMapper mapper) + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } - public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + public async Task Handle(AddUserCommand request, CancellationToken cancellationToken) { var user = await _context.Users.FirstOrDefaultAsync( x => x.Username.Equals(request.Username) || x.Email.Equals(request.Email), cancellationToken); if (user is not null) { - throw new ConflictException("Username or Email has been taken"); + throw new ConflictException("Username or Email has been taken."); } var department = await _context.Departments @@ -47,7 +47,7 @@ public async Task Handle(CreateUserCommand request, CancellationToken c if (department is null) { - throw new KeyNotFoundException("Department does not exist"); + throw new KeyNotFoundException("Department does not exist."); } var entity = new User @@ -55,8 +55,8 @@ public async Task Handle(CreateUserCommand request, CancellationToken c Username = request.Username, PasswordHash = SecurityUtil.Hash(request.Password), Email = request.Email, - FirstName = request.FirstName.Trim(), - LastName = request.LastName.Trim(), + FirstName = request.FirstName?.Trim(), + LastName = request.LastName?.Trim(), Department = department, Role = request.Role, Position = request.Position, diff --git a/src/Application/Users/Commands/CreateUser/CreateUserCommandValidator.cs b/src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs similarity index 67% rename from src/Application/Users/Commands/CreateUser/CreateUserCommandValidator.cs rename to src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs index cfd82b1d..16d07c05 100644 --- a/src/Application/Users/Commands/CreateUser/CreateUserCommandValidator.cs +++ b/src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs @@ -1,10 +1,11 @@ +using Application.Identity; using FluentValidation; -namespace Application.Users.Commands.CreateUser; +namespace Application.Users.Commands.AddUser; -public class CreateUserCommandValidator : AbstractValidator +public class AddUserCommandValidator : AbstractValidator { - public CreateUserCommandValidator() + public AddUserCommandValidator() { RuleLevelCascadeMode = CascadeMode.Stop; @@ -13,32 +14,30 @@ public CreateUserCommandValidator() .MaximumLength(50).WithMessage("Username cannot exceed 64 characters."); RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required.") .EmailAddress().WithMessage("Require valid email.") - .MaximumLength(320).WithMessage("Email length too long"); + .MaximumLength(320).WithMessage("Email length too long."); RuleFor(x => x.Password) - .NotEmpty().WithMessage("Your password cannot be empty"); + .NotEmpty().WithMessage("Password is required."); RuleFor(x => x.Role) - .NotEmpty().WithMessage("Role cannot be empty.") + .NotEmpty().WithMessage("Role is required.") .MaximumLength(64).WithMessage("Role cannot exceed 64 characters.") .Must(BeNotAdmin).WithMessage("Cannot add a user as Administrator."); RuleFor(x => x.FirstName) - .NotEmpty().WithMessage("First name is required.") .MaximumLength(50).WithMessage("First name cannot exceed 50 characters."); RuleFor(x => x.LastName) - .NotEmpty().WithMessage("Last name is required.") .MaximumLength(50).WithMessage("Last name cannot exceed 50 characters."); RuleFor(x => x.Position) - .NotEmpty().WithMessage("Position cannot be empty.") .MaximumLength(64).WithMessage("Position cannot exceed 64 characters."); } private static bool BeNotAdmin(string role) { - return !role.Equals("Administrator"); + return !role.Equals(IdentityData.Roles.Admin); } } \ No newline at end of file diff --git a/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs b/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs index 505961e6..43abac74 100644 --- a/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs +++ b/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Users.Queries; using AutoMapper; @@ -31,7 +32,7 @@ public async Task Handle(DisableUserCommand request, CancellationToken if (!user.IsActive) { - throw new InvalidOperationException("User has already been disabled."); + throw new ConflictException("User has already been disabled."); } user.IsActive = false; diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 3411d01a..7e0d22c0 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -1,4 +1,4 @@ -using Application.Departments.Commands.CreateDepartment; +using Application.Departments.Commands.AddDepartment; using Bogus; using Domain.Common; using Domain.Entities; @@ -15,7 +15,7 @@ namespace Application.Tests.Integration; [Collection(nameof(BaseCollectionFixture))] public class BaseClassFixture { - protected readonly Faker _departmentGenerator = new Faker() + protected readonly Faker _departmentGenerator = new Faker() .RuleFor(x => x.Name, faker => faker.Commerce.Department()); protected static IServiceScopeFactory _scopeFactory = null!; diff --git a/tests/Application.Tests.Integration/CustomApiFactory.cs b/tests/Application.Tests.Integration/CustomApiFactory.cs index 04d44731..9d30c073 100644 --- a/tests/Application.Tests.Integration/CustomApiFactory.cs +++ b/tests/Application.Tests.Integration/CustomApiFactory.cs @@ -28,7 +28,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) var databaseSettings = GetConfiguration().GetSection(nameof(DatabaseSettings)).Get(); services.AddDbContext(options => { - options.UseNpgsql(databaseSettings.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); + options.UseNpgsql(databaseSettings?.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); }); }); } diff --git a/tests/Application.Tests.Integration/Departments/Commands/CreateDepartmentTests.cs b/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs similarity index 88% rename from tests/Application.Tests.Integration/Departments/Commands/CreateDepartmentTests.cs rename to tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs index 0e41af6c..8823a9fb 100644 --- a/tests/Application.Tests.Integration/Departments/Commands/CreateDepartmentTests.cs +++ b/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs @@ -6,9 +6,9 @@ namespace Application.Tests.Integration.Departments.Commands; -public class CreateDepartmentTests : BaseClassFixture +public class AddDepartmentTests : BaseClassFixture { - public CreateDepartmentTests(CustomApiFactory apiFactory) : base(apiFactory) + public AddDepartmentTests(CustomApiFactory apiFactory) : base(apiFactory) { } @@ -40,7 +40,7 @@ public async Task ShouldReturnConflict_WhenDepartmentNameHasExisted() var action = async () => await SendAsync(createDepartmentCommand); // Assert - await action.Should().ThrowAsync().WithMessage("Department name already exists"); + await action.Should().ThrowAsync().WithMessage("Department name already exists."); // Cleanup var departmentEntity = await FindAsync(department.Id); diff --git a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs index db3c863c..8832e0c3 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs @@ -2,7 +2,7 @@ using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands.AddFolder; using Application.Lockers.Commands.AddLocker; -using Application.Rooms.Commands.CreateRoom; +using Application.Rooms.Commands.AddRoom; using Bogus; using Domain.Entities.Physical; using Domain.Exceptions; @@ -18,7 +18,7 @@ public class AddFolderTests : BaseClassFixture .RuleFor(f => f.Description, faker => faker.Commerce.ProductDescription()) .RuleFor(f => f.Capacity, faker => faker.Random.Int(1,9999)); - private readonly Faker _roomGenerator = new Faker() + private readonly Faker _roomGenerator = new Faker() .RuleFor(r => r.Name, faker => faker.Commerce.ProductName()) .RuleFor(r => r.Description, faker => faker.Commerce.ProductDescription()) .RuleFor(r => r.Capacity, faker => faker.Random.Int(1,9999)); diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs index 80528c3c..1dee5f9d 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Folders.Commands.DisableFolder; using Domain.Entities.Physical; using FluentAssertions; @@ -121,7 +122,7 @@ public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabl var result = async () => await SendAsync(disableFolderCommand); // Assert - await result.Should().ThrowAsync() + await result.Should().ThrowAsync() .WithMessage("Folder has already been disabled."); // Cleanup diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs index 5cedf272..8f3d30e0 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs @@ -91,7 +91,7 @@ public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameR var action = async () => await SendAsync(addLockerCommand); // Assert - await action.Should().ThrowAsync().WithMessage("Locker's name already exists."); + await action.Should().ThrowAsync().WithMessage("Locker name already exists."); // Cleanup var roomEntity = await FindAsync(room.Id); diff --git a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs index 8b83f4da..9fefabc4 100644 --- a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs @@ -112,7 +112,7 @@ public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotAvailable() var action = async () => await SendAsync(command); // Assert - await action.Should().ThrowAsync() + await action.Should().ThrowAsync() .WithMessage("Room have already been disabled."); // Cleanup diff --git a/tests/Application.Tests.Integration/Users/Commands/CreateUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs similarity index 87% rename from tests/Application.Tests.Integration/Users/Commands/CreateUserTests.cs rename to tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs index 53c1fbdb..52a431c9 100644 --- a/tests/Application.Tests.Integration/Users/Commands/CreateUserTests.cs +++ b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs @@ -1,4 +1,4 @@ -using Application.Users.Commands.CreateUser; +using Application.Users.Commands.AddUser; using Bogus; using Domain.Entities; using FluentAssertions; @@ -6,9 +6,9 @@ namespace Application.Tests.Integration.Users.Commands; -public class CreateUserTests : BaseClassFixture +public class AddUserTests : BaseClassFixture { - private readonly Faker _userGenerator = new Faker() + private readonly Faker _userGenerator = new Faker() .RuleFor(x => x.Username, faker => faker.Person.UserName) .RuleFor(x => x.Email, faker => faker.Person.Email) .RuleFor(x => x.FirstName, faker => faker.Person.FirstName) @@ -16,7 +16,7 @@ public class CreateUserTests : BaseClassFixture .RuleFor(x => x.Password, faker => faker.Random.String()) .RuleFor(x => x.Role, faker => faker.Random.Word()) .RuleFor(x => x.Position, faker => faker.Random.Word()); - public CreateUserTests(CustomApiFactory apiFactory) : base(apiFactory) + public AddUserTests(CustomApiFactory apiFactory) : base(apiFactory) { } From 9bfc699835c876930b38d808b6f7abdaa37f8c89 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 27 May 2023 19:38:46 +0700 Subject: [PATCH 010/162] Update/auth mechanism (#107) * update: refresh and login endpoint now delete db refresh tokens and refresh tokens are now persistent instead of fleeting * update: refresh token rotation --- src/Api/Controllers/AuthController.cs | 22 ++++++++++++++----- .../Identity/IdentityService.cs | 15 +++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index dac02af4..30291352 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -1,4 +1,5 @@ using System.IdentityModel.Tokens.Jwt; +using System.Security.Authentication; using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Responses; using Application.Common.Interfaces; @@ -71,12 +72,21 @@ public async Task Refresh() { var refreshToken = Request.Cookies[nameof(RefreshToken)]; var jweToken = Request.Cookies["JweToken"]; - - var authResult = await _identityService.RefreshTokenAsync(jweToken!, refreshToken!); - - SetRefreshToken(authResult.RefreshToken); - SetJweToken(authResult.Token, authResult.RefreshToken); - + + try + { + var authResult = await _identityService.RefreshTokenAsync(jweToken!, refreshToken!); + + SetRefreshToken(authResult.RefreshToken); + SetJweToken(authResult.Token, authResult.RefreshToken); + } + catch (AuthenticationException) + { + RemoveJweToken(); + RemoveRefreshToken(); + throw; + } + return Ok(); } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index eff63f43..7f879fdd 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,6 +1,7 @@ using System.Data; using System.Globalization; using System.IdentityModel.Tokens.Jwt; +using System.Reflection.Metadata.Ecma335; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography; @@ -149,19 +150,9 @@ public async Task RefreshTokenAsync(string token, string r throw new AuthenticationException("This refresh token does not match this Jwt."); } - var jweToken = CreateJweToken(user); + var result = await GenerateAuthenticationResultForUserAsync(user); - storedRefreshToken.JwtId = jweToken.Id; - storedRefreshToken.ExpiryDateTime = - LocalDateTime.FromDateTime(DateTime.UtcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)); - _context.RefreshTokens.Update(storedRefreshToken); - await _context.SaveChangesAsync(); - - return new AuthenticationResult() - { - Token = jweToken, - RefreshToken = _mapper.Map(storedRefreshToken) - }; + return result; } private ClaimsPrincipal? GetPrincipalFromToken(string token) From 7cf694c51b0cb7e439743ee5ea9b06b21a37df63 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 27 May 2023 20:02:48 +0700 Subject: [PATCH 011/162] Add/queries commands (#108) * add: GetDepartmentById, UpdateDepartment, DeleteDepartment, EnableRoom endpoints * add: update room command and endpoint * add: get by id command and endpoint * add: documentation on endpoints I forgot to add * fix: change annotation with some documentation and get all room paginated endpoint * fix: change annotation with some documentation and get all room paginated endpoint pt2 * add: remove locker endpoint and command * add: update locker command and request * add: get locker by id endpoint and command * add: get all lockers paginated query and endpoint * add: enable folder command and endpoint * add: remove folder command and endpoint * add: get folder by id query and endpoint * add: get all folders paginated query and endpoint * add: update folder command and endpoint * add: update document command and endpoint * add: delete document command and endpoint * add: update user command and endpoint * add: get user by id query and endpoint * add: get all users paginated query and endpoint * add: remove staff from room command and endpoint * add: get staff by id query and endpoint * add: get all staffs paginated query and endpoint * add: get staff by room query and endpoint * chore: merge from dev * add: enable user command and endpoint --- src/Api/Controllers/AuthController.cs | 1 - src/Api/Controllers/DepartmentsController.cs | 66 ++++++++- src/Api/Controllers/DocumentsController.cs | 47 +++++++ src/Api/Controllers/FoldersController.cs | 125 ++++++++++++++++++ src/Api/Controllers/LockersController.cs | 94 ++++++++++++- .../Documents/UpdateDocumentRequest.cs | 8 ++ .../GetAllFoldersPaginatedQueryParameters.cs | 11 ++ .../Requests/Folders/UpdateFolderRequest.cs | 8 ++ .../GetAllLockersPaginatedQueryParameters.cs | 13 ++ .../Requests/Lockers/UpdateLockerRequest.cs | 8 ++ .../GetAllStaffsPaginatedQueryParameters.cs | 10 ++ .../Staffs/RemoveStaffFromRoomRequest.cs | 6 + .../Requests/UpdateDepartmentRequest.cs | 6 + .../Payload/Requests/UpdateRoomRequest.cs | 8 ++ .../GetAllUsersPaginatedQueryParameters.cs | 11 ++ .../Requests/Users/UpdateUserRequest.cs | 11 ++ src/Api/Controllers/RoomsController.cs | 89 +++++++++++++ src/Api/Controllers/StaffsController.cs | 87 ++++++++++++ src/Api/Controllers/UsersController.cs | 112 ++++++++++++++-- .../UpdateDepartmentCommand.cs | 10 ++ .../GetDepartmentByIdQuery.cs | 9 ++ .../DeleteDocument/DeleteDocumentCommand.cs | 9 ++ .../UpdateDocument/UpdateDocumentCommand.cs | 11 +- .../EnableFolder/EnableFolderCommand.cs | 9 ++ .../RemoveFolder/RemoveFolderCommand.cs | 9 ++ .../UpdateFolder/UpdateFolderCommand.cs | 12 ++ .../GetAllFoldersPaginatedQuery.cs | 15 +++ .../GetFolderById/GetFolderByIdQuery.cs | 9 ++ .../RemoveLocker/RemoveLockerCommand.cs | 9 ++ .../UpdateLocker/UpdateLockerCommand.cs | 12 ++ .../GetAllLockersPaginatedQuery.cs | 14 ++ .../GetLockerById/GetLockerByIdQuery.cs | 9 ++ .../Commands/EnableRoom/EnableRoomCommand.cs | 9 ++ .../Commands/UpdateRoom/UpdateRoomCommand.cs | 12 ++ .../GetAllRoomsPaginatedQuery.cs | 13 ++ .../Queries/GetRoomById/GetRoomByIdQuery.cs | 9 ++ .../RemoveStaffFromRoomCommand.cs | 10 ++ .../GetAllStaffsPaginatedQuery.cs | 14 ++ .../Queries/GetStaffById/GetStaffByIdQuery.cs | 9 ++ .../GetStaffByRoom/GetStaffByRoomQuery.cs | 9 ++ .../Commands/EnableUser/EnableUserCommand.cs | 9 ++ .../Commands/UpdateUser/UpdateUserCommand.cs | 15 +++ .../GetAllUsersPaginatedQuery.cs | 14 ++ .../Queries/GetUserById/GetUserByIdQuery.cs | 8 ++ 44 files changed, 971 insertions(+), 18 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs create mode 100644 src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs create mode 100644 src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs create mode 100644 src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs create mode 100644 src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs create mode 100644 src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs create mode 100644 src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs create mode 100644 src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs create mode 100644 src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs create mode 100644 src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs create mode 100644 src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs create mode 100644 src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs create mode 100644 src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs create mode 100644 src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs create mode 100644 src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs create mode 100644 src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs create mode 100644 src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs create mode 100644 src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs create mode 100644 src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs create mode 100644 src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs create mode 100644 src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs create mode 100644 src/Application/Users/Commands/EnableUser/EnableUserCommand.cs create mode 100644 src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs create mode 100644 src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs create mode 100644 src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 30291352..efdd1591 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -104,7 +104,6 @@ private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken var cookieOptions = new CookieOptions { HttpOnly = true, - Expires = newRefreshToken.ExpiryDateTime }; var handler = new JwtSecurityTokenHandler(); Response.Cookies.Append("JweToken", handler.WriteToken(jweToken), cookieOptions); diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 465738a9..98045cf7 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -1,6 +1,10 @@ -using Application.Common.Models; +using Api.Controllers.Payload.Requests; +using Application.Common.Models; +using Application.Departments.Commands.DeleteDepartment; +using Application.Departments.Commands.UpdateDepartment; using Application.Departments.Commands.AddDepartment; using Application.Departments.Queries.GetAllDepartments; +using Application.Departments.Queries.GetDepartmentById; using Application.Identity; using Application.Users.Queries; using Infrastructure.Identity.Authorization; @@ -38,4 +42,64 @@ public async Task>>> GetAllDepart var result = await Mediator.Send(new GetAllDepartmentsQuery()); return Ok(Result>.Succeed(result)); } + + /// + /// Get back a department based on its id + /// + /// id of the department to be retrieved + /// A DepartmentDto of the retrieved department + [HttpGet("{departmentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetDepartmentById([FromRoute] Guid departmentId) + { + var query = new GetDepartmentByIdQuery() + { + DepartmentId = departmentId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Update a department + /// + /// Id of the department to be updated + /// Update department details + /// A DepartmentDto of the updated department + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPut("{departmentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> UpdateDepartment([FromRoute] Guid departmentId, [FromBody] UpdateDepartmentRequest request) + { + var command = new UpdateDepartmentCommand() + { + DepartmentId = departmentId, + Name = request.Name + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Delete a department + /// + /// Id of the department to be deleted + /// A DepartmentDto of the deleted department + [RequiresRole(IdentityData.Roles.Admin)] + [HttpDelete("{departmentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> DeleteDepartment([FromRoute] Guid departmentId) + { + var command = new DeleteDepartmentCommand() + { + DepartmentId = departmentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index a4b4d8bf..41f8fb3d 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,6 +1,9 @@ +using Api.Controllers.Payload.Requests.Documents; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using Application.Documents.Commands.DeleteDocument; using Application.Documents.Commands.ImportDocument; +using Application.Documents.Commands.UpdateDocument; using Application.Documents.Queries.GetAllDocumentsPaginated; using Application.Documents.Queries.GetDocumentById; using Application.Documents.Queries.GetDocumentTypes; @@ -70,4 +73,48 @@ public async Task>> GetDocumentById(Guid id) var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Update a document + /// + /// Id of the document to be updated + /// Update document details + /// A DocumentDto of the updated document + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPut("{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> UpdateDocument([FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) + { + var query = new UpdateDocumentCommand() + { + DocumentId = documentId, + Title = request.Title, + Description = request.Description, + DocumentType = request.DocumentType + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Delete a document + /// + /// Id of the document to be deleted + /// A DocumentDto of the deleted document + [HttpDelete("{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> UpdateDocument([FromRoute] Guid documentId) + { + var query = new DeleteDocumentCommand() + { + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index e37a6691..8ea03dd6 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,7 +1,13 @@ +using Api.Controllers.Payload.Requests.Folders; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands.AddFolder; using Application.Folders.Commands.DisableFolder; +using Application.Folders.Commands.EnableFolder; +using Application.Folders.Commands.RemoveFolder; +using Application.Folders.Commands.UpdateFolder; +using Application.Folders.Queries.GetAllFoldersPaginated; +using Application.Folders.Queries.GetFolderById; using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -10,7 +16,57 @@ namespace Api.Controllers; public class FoldersController : ApiControllerBase { + /// + /// Get a folder by id + /// + /// Id of the folder to be retrieved + /// A FolderDto of the retrieved folder [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet("{folderId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid folderId) + { + var query = new GetFolderByIdQuery() + { + FolderId = folderId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all folders paginated + /// + /// Get all folders query parameters + /// A paginated list of FolderDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllFoldersPaginatedQueryParameters queryParameters) + { + var query = new GetAllFoldersPaginatedQuery() + { + RoomId = queryParameters.RoomId, + LockerId = queryParameters.LockerId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Add a folder + /// + /// Add folder details + /// A FolderDto of the added folder + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -22,7 +78,52 @@ public async Task>> AddFolder([FromBody] AddFolde var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Remove a folder + /// + /// Id of the folder to be removed + /// A FolderDto of the removed folder + [RequiresRole(IdentityData.Roles.Admin)] + [HttpDelete("{folderId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RemoveFolder([FromRoute] Guid folderId) + { + var command = new RemoveFolderCommand() + { + FolderId = folderId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Enable a folder + /// + /// Enable folder details + /// A FolderDto of the enabled folder + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpPut("enable")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> EnableFolder([FromBody] EnableFolderCommand command) + { + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// + /// Disable a folder + /// + /// Disable folder details + /// A FolderDto of the disabled folder [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("disable")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -35,4 +136,28 @@ public async Task>> DisableFolder([FromBody] Disa var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Update a folder + /// + /// Id of the folder to be updated + /// Update folder details + /// A FolderDto of the updated folder + [HttpPut("{folderId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) + { + var command = new UpdateFolderCommand() + { + FolderId = folderId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 350ca406..f064090e 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,9 +1,15 @@ -using Application.Common.Models; +using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Lockers; +using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Lockers.Commands.AddLocker; using Application.Lockers.Commands.DisableLocker; using Application.Lockers.Commands.EnableLocker; +using Application.Lockers.Commands.RemoveLocker; +using Application.Lockers.Commands.UpdateLocker; +using Application.Lockers.Queries.GetAllLockersPaginated; +using Application.Lockers.Queries.GetLockerById; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -11,6 +17,48 @@ namespace Api.Controllers; public class LockersController : ApiControllerBase { + /// + /// Get a locker by id + /// + /// Id of the locker to be retrieved + /// A LockerDto of the retrieved locker + [HttpGet("{lockerId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid lockerId) + { + var query = new GetLockerByIdQuery() + { + LockerId = lockerId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all lockers paginated + /// + /// Get all lockers query parameters + /// A paginated list of LockerDto + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) + { + var query = new GetAllLockersPaginatedQuery() + { + RoomId = queryParameters.RoomId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] @@ -49,4 +97,48 @@ public async Task>> EnableLocker([FromBody] Enabl var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Update a locker + /// + /// Id of the locker to be updated + /// Update locker details + /// A LockerDto of the updated locker + [HttpPut("{lockerId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) + { + var command = new UpdateLockerCommand() + { + LockerId = lockerId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Remove a locker + /// + /// Id of the locker to be removed + /// A LockerDto of the removed locker + [HttpDelete("{lockerId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Remove([FromRoute] Guid lockerId) + { + var command = new RemoveLockerCommand() + { + LockerId = lockerId + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } diff --git a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs new file mode 100644 index 00000000..e6a0885b --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class UpdateDocumentRequest +{ + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string DocumentType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs new file mode 100644 index 00000000..db642122 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -0,0 +1,11 @@ +namespace Api.Controllers.Payload.Requests.Folders; + +public class GetAllFoldersPaginatedQueryParameters +{ + public Guid? RoomId { get; set; } + public Guid? LockerId { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs new file mode 100644 index 00000000..97e9691f --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Folders; + +public class UpdateFolderRequest +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int Capacity { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs new file mode 100644 index 00000000..b4b7f837 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers.Payload.Requests.Lockers; + + +public class GetAllLockersPaginatedQueryParameters +{ + public Guid? RoomId { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs new file mode 100644 index 00000000..a7f69036 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Lockers; + +public class UpdateLockerRequest +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int Capacity { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs new file mode 100644 index 00000000..bcb47cb4 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs @@ -0,0 +1,10 @@ +namespace Api.Controllers.Payload.Requests.Staffs; + +public class GetAllStaffsPaginatedQueryParameters +{ + public string? SearchTerm { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs new file mode 100644 index 00000000..811608df --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Staffs; + +public class RemoveStaffFromRoomRequest +{ + public Guid RoomId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs b/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs new file mode 100644 index 00000000..467ceeb5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests; + +public class UpdateDepartmentRequest +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs b/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs new file mode 100644 index 00000000..a579a447 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests; + +public class UpdateRoomRequest +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int Capacity { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs new file mode 100644 index 00000000..d3484660 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -0,0 +1,11 @@ +namespace Api.Controllers.Payload.Requests.Users; + +public class GetAllUsersPaginatedQueryParameters +{ + public Guid? DepartmentId { get; set; } + public string? SearchTerm { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs new file mode 100644 index 00000000..77bcb0e4 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs @@ -0,0 +1,11 @@ +namespace Api.Controllers.Payload.Requests.Users; + +public class UpdateUserRequest +{ + public string Username { get; set; } = null!; + public string Email { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string Role { get; set; } = null!; + public string? Position { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 39c156e9..3cbeb8dd 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,10 +1,15 @@ +using Api.Controllers.Payload.Requests; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Rooms.Commands.AddRoom; using Application.Rooms.Commands.DisableRoom; +using Application.Rooms.Commands.EnableRoom; using Application.Rooms.Commands.RemoveRoom; +using Application.Rooms.Commands.UpdateRoom; +using Application.Rooms.Queries.GetAllRoomPaginated; using Application.Rooms.Queries.GetEmptyContainersPaginated; +using Application.Rooms.Queries.GetRoomById; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -59,4 +64,88 @@ public async Task>> RemoveRoom(RemoveRoomCommand co var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Enable a room + /// + /// Enable room details + /// A RoomDto of the enabled room + [HttpPut("enable")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> EnableRoom([FromBody] EnableRoomCommand command) + { + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Update a room + /// + /// Id of the room to be updated + /// Update room details + /// A RoomDto of the updated room + [HttpPut("{roomId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) + { + Console.WriteLine(request.Description); + var command = new UpdateRoomCommand() + { + RoomId = roomId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Get a room by id + /// + /// Id of the room to be retrieved + /// A RoomDto of the retrieved room + [HttpGet("{roomId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid roomId) + { + var query = new GetRoomByIdQuery() + { + RoomId = roomId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all rooms paginated + /// + /// The page index + /// The size number + /// Criteria + /// The order in which the rooms are sorted + /// A paginated list of rooms + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated(int? page, int? size, string? sortBy, string? sortOrder) + { + var query = new GetAllRoomsPaginatedQuery() + { + Page = page, + Size = size, + SortBy = sortBy, + SortOrder = sortOrder + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index d714b2bf..1ad6fff1 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -1,5 +1,10 @@ +using Api.Controllers.Payload.Requests.Staffs; using Application.Common.Models; using Application.Identity; +using Application.Staffs.Commands.RemoveStaffFromRoom; +using Application.Staffs.Queries.GetAllStaffsPaginated; +using Application.Staffs.Queries.GetStaffById; +using Application.Staffs.Queries.GetStaffByRoom; using Application.Staffs.Commands.AddStaff; using Application.Users.Queries.Physical; using Infrastructure.Identity.Authorization; @@ -9,6 +14,66 @@ namespace Api.Controllers; public class StaffsController : ApiControllerBase { + /// + /// Get a staff by id + /// + /// Id of the staff to be retrieved + /// A StaffDto of the retrieved staff + [HttpGet("{staffId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid staffId) + { + var query = new GetStaffByIdQuery() + { + StaffId = staffId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get a staff by room + /// + /// Id of the room to retrieve staff + /// A StaffDto of the retrieved staff + [HttpGet("get-by-room/{roomId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetByRoom([FromRoute] Guid roomId) + { + var query = new GetStaffByRoomQuery() + { + RoomId = roomId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all staffs paginated + /// + /// Get all staffs query parameters + /// A paginated list of StaffDto + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllStaffsPaginatedQueryParameters queryParameters) + { + var query = new GetAllStaffsPaginatedQuery() + { + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] @@ -19,4 +84,26 @@ public async Task>> AddStaff([FromBody] AddStaffCo var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Remove a staff from room + /// + /// Id of the staff to be removed from room + /// Remove details + /// A StaffDto of the removed staff + [HttpPut("{staffId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> RemoveStaffFromRoom([FromRoute] Guid staffId, + [FromBody] RemoveStaffFromRoomRequest request) + { + var command = new RemoveStaffFromRoomCommand() + { + StaffId = staffId, + RoomId = request.RoomId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 1d51f7f0..f7d93173 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,8 +1,14 @@ +using Api.Controllers.Payload.Requests.Users; using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Users.Commands.AddUser; using Application.Users.Commands.DisableUser; +using Application.Users.Commands.EnableUser; +using Application.Users.Commands.UpdateUser; using Application.Users.Queries; +using Application.Users.Queries.GetAllUsersPaginated; +using Application.Users.Queries.GetUserById; using Application.Users.Queries.GetUsersByName; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Authorization; @@ -12,6 +18,49 @@ namespace Api.Controllers; public class UsersController : ApiControllerBase { + /// + /// Get a user by id + /// + /// Id of the user to be retrieved + /// A UserDto of the retrieved user + [HttpGet("{userId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetUserById([FromRoute] Guid userId) + { + var query = new GetUserByIdQuery + { + UserId = userId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all users paginated + /// + /// Get all users query parameters + /// A paginated list of UserDto + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllUsersPaginatedQueryParameters queryParameters) + { + var query = new GetAllUsersPaginatedQuery() + { + DepartmentId = queryParameters.DepartmentId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] @@ -25,20 +74,40 @@ public async Task>> AddUser([FromBody] AddUserComma return Ok(Result.Succeed(result)); } + // [RequiresRole(IdentityData.Roles.Admin)] + // [HttpGet] + // [ProducesResponseType(StatusCodes.Status200OK)] + // [ProducesResponseType(StatusCodes.Status403Forbidden)] + // public async Task>>> GetUsersByName(string? searchTerm, int? page, int? size) + // { + // var query = new GetUsersByNameQuery + // { + // SearchTerm = searchTerm, + // Page = page, + // Size = size + // }; + // var result = await Mediator.Send(query); + // return Ok(Result>.Succeed(result)); + // } + + /// + /// Disable a user + /// + /// Id of the user to be enabled + /// A UserDto of the enabled user [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet] + [HttpPost("enable/{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetUsersByName(string? searchTerm, int? page, int? size) + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> EnableUser([FromRoute] Guid userId) { - var query = new GetUsersByNameQuery + var command = new EnableUserCommand() { - SearchTerm = searchTerm, - Page = page, - Size = size + UserId = userId }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); } [RequiresRole(IdentityData.Roles.Admin)] @@ -52,4 +121,31 @@ public async Task>> DisableUser([FromBody] DisableU var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Update a user + /// + /// Id of the user to be updated + /// Update user details + /// A UserDto of the updated user + [HttpPut("{userId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) + { + var command = new UpdateUserCommand() + { + UserId = userId, + Username = request.Username, + Email = request.Email, + FirstName = request.FirstName, + LastName = request.LastName, + Role = request.Role, + Position = request.Position, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } diff --git a/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs b/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs new file mode 100644 index 00000000..941879af --- /dev/null +++ b/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs @@ -0,0 +1,10 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Departments.Commands.UpdateDepartment; + +public record UpdateDepartmentCommand : IRequest +{ + public Guid DepartmentId { get; set; } + public string Name { get; init; } = null!; +} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs b/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs new file mode 100644 index 00000000..fe0ec11a --- /dev/null +++ b/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs @@ -0,0 +1,9 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Departments.Queries.GetDepartmentById; + +public record GetDepartmentByIdQuery : IRequest +{ + public Guid DepartmentId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs b/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs new file mode 100644 index 00000000..eefecf87 --- /dev/null +++ b/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Documents.Commands.DeleteDocument; + +public record DeleteDocumentCommand : IRequest +{ + public Guid DocumentId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs b/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs index 96e9dc8d..d7b06736 100644 --- a/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs +++ b/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs @@ -5,11 +5,8 @@ namespace Application.Documents.Commands.UpdateDocument; public record UpdateDocumentCommand : IRequest { - public Guid Id { get; set; } - public string? Title { get; set; } - public string? Description { get; set; } - public string? DocumentType { get; set; } - public Guid? Department { get; set; } - public Guid? Importer { get; set; } - public Guid? Folder { get; set; } + public Guid DocumentId { get; init; } + public string Title { get; init; } = null!; + public string? Description { get; init; } + public string DocumentType { get; init; } = null!; } \ No newline at end of file diff --git a/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs b/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs new file mode 100644 index 00000000..eb7f49f9 --- /dev/null +++ b/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands.EnableFolder; + +public record EnableFolderCommand : IRequest +{ + public Guid FolderId { get; init; } +} diff --git a/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs b/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs new file mode 100644 index 00000000..91db8b5e --- /dev/null +++ b/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands.RemoveFolder; + +public record RemoveFolderCommand : IRequest +{ + public Guid FolderId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs b/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs new file mode 100644 index 00000000..30ebea09 --- /dev/null +++ b/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands.UpdateFolder; + +public record UpdateFolderCommand : IRequest +{ + public Guid FolderId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs new file mode 100644 index 00000000..ecd9fbb2 --- /dev/null +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs @@ -0,0 +1,15 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Queries.GetAllFoldersPaginated; + +public record GetAllFoldersPaginatedQuery : IRequest> +{ + public Guid? RoomId { get; init; } + public Guid? LockerId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs b/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs new file mode 100644 index 00000000..bf422997 --- /dev/null +++ b/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Queries.GetFolderById; + +public record GetFolderByIdQuery : IRequest +{ + public Guid FolderId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs b/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs new file mode 100644 index 00000000..412df4b7 --- /dev/null +++ b/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Commands.RemoveLocker; + +public record RemoveLockerCommand : IRequest +{ + public Guid LockerId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs b/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs new file mode 100644 index 00000000..540ceeef --- /dev/null +++ b/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Commands.UpdateLocker; + +public record UpdateLockerCommand : IRequest +{ + public Guid LockerId { get; set; } + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int Capacity { get; init; } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs new file mode 100644 index 00000000..4e0e8eb7 --- /dev/null +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs @@ -0,0 +1,14 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Queries.GetAllLockersPaginated; + +public record GetAllLockersPaginatedQuery : IRequest> +{ + public Guid? RoomId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs b/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs new file mode 100644 index 00000000..1a592104 --- /dev/null +++ b/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Queries.GetLockerById; + +public record GetLockerByIdQuery : IRequest +{ + public Guid LockerId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs b/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs new file mode 100644 index 00000000..d89c52a5 --- /dev/null +++ b/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Commands.EnableRoom; + +public record EnableRoomCommand : IRequest +{ + public Guid RoomId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs b/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs new file mode 100644 index 00000000..2e603d9d --- /dev/null +++ b/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Commands.UpdateRoom; + +public record UpdateRoomCommand : IRequest +{ + public Guid RoomId { get; init; } + public string Name { get; set; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs b/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs new file mode 100644 index 00000000..c691570f --- /dev/null +++ b/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs @@ -0,0 +1,13 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Queries.GetAllRoomPaginated; + +public record GetAllRoomsPaginatedQuery : IRequest> +{ + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs b/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs new file mode 100644 index 00000000..638e9b51 --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs @@ -0,0 +1,9 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Queries.GetRoomById; + +public record GetRoomByIdQuery : IRequest +{ + public Guid RoomId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs new file mode 100644 index 00000000..c25f18ca --- /dev/null +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs @@ -0,0 +1,10 @@ +using Application.Users.Queries.Physical; +using MediatR; + +namespace Application.Staffs.Commands.RemoveStaffFromRoom; + +public record RemoveStaffFromRoomCommand : IRequest +{ + public Guid StaffId { get; init; } + public Guid RoomId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs new file mode 100644 index 00000000..a640b907 --- /dev/null +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs @@ -0,0 +1,14 @@ +using Application.Common.Models; +using Application.Users.Queries.Physical; +using MediatR; + +namespace Application.Staffs.Queries.GetAllStaffsPaginated; + +public class GetAllStaffsPaginatedQuery : IRequest> +{ + public string? SearchTerm { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs b/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs new file mode 100644 index 00000000..7efed7f2 --- /dev/null +++ b/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs @@ -0,0 +1,9 @@ +using Application.Users.Queries.Physical; +using MediatR; + +namespace Application.Staffs.Queries.GetStaffById; + +public record GetStaffByIdQuery : IRequest +{ + public Guid StaffId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs b/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs new file mode 100644 index 00000000..0d3d8a1f --- /dev/null +++ b/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs @@ -0,0 +1,9 @@ +using Application.Users.Queries.Physical; +using MediatR; + +namespace Application.Staffs.Queries.GetStaffByRoom; + +public record GetStaffByRoomQuery : IRequest +{ + public Guid RoomId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs b/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs new file mode 100644 index 00000000..047f13d5 --- /dev/null +++ b/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs @@ -0,0 +1,9 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Users.Commands.EnableUser; + +public record EnableUserCommand : IRequest +{ + public Guid UserId { get; init; } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs b/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs new file mode 100644 index 00000000..0f3736ea --- /dev/null +++ b/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs @@ -0,0 +1,15 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Users.Commands.UpdateUser; + +public record UpdateUserCommand : IRequest +{ + public Guid UserId { get; init; } + public string Username { get; init; } = null!; + public string Email { get; init; } = null!; + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string Role { get; init; } = null!; + public string? Position { get; init; } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs b/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs new file mode 100644 index 00000000..0c3146c7 --- /dev/null +++ b/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs @@ -0,0 +1,14 @@ +using Application.Common.Models; +using MediatR; + +namespace Application.Users.Queries.GetAllUsersPaginated; + +public record GetAllUsersPaginatedQuery : IRequest> +{ + public Guid? DepartmentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs b/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs new file mode 100644 index 00000000..9f2e60d7 --- /dev/null +++ b/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Application.Users.Queries.GetUserById; + +public record GetUserByIdQuery : IRequest +{ + public Guid UserId { get; init; } +} \ No newline at end of file From e2130808f619aaed6a00f2b78a4e2050d075572b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Mon, 29 May 2023 12:37:46 +0700 Subject: [PATCH 012/162] Refactor/baseline (#109) * refactor: controller endpoints * refactor: application features' name * refactor: remove some dependencies from tests * refactor: swagger schema and controller parameters * add: base class fixture methods * refactor: alter some schema * fix: add folder tests * refactor: again * add: api documentation * fix: i forgot this * migrations: a room now link to a department, fixed all problems related to tests --- docker-compose.test.yml | 2 +- src/Api/Api.csproj | 1 + src/Api/ConfigureServices.cs | 15 +- src/Api/Controllers/AuthController.cs | 54 +- src/Api/Controllers/DepartmentsController.cs | 65 +- src/Api/Controllers/DocumentsController.cs | 109 ++- src/Api/Controllers/FoldersController.cs | 56 +- src/Api/Controllers/LockersController.cs | 102 ++- .../Payload/Requests/Auth/LoginModel.cs | 16 + .../Requests/Auth/RefreshTokenRequest.cs | 16 + .../Departments/AddDepartmentRequest.cs | 12 + .../Departments/UpdateDepartmentRequest.cs | 12 + ...GetAllDocumentsPaginatedQueryParameters.cs | 40 + .../Documents/ImportDocumentRequest.cs | 28 + .../Documents/UpdateDocumentRequest.cs | 12 + .../Requests/Folders/AddFolderRequest.cs | 23 + .../GetAllFoldersPaginatedQueryParameters.cs | 21 + .../Requests/Folders/UpdateFolderRequest.cs | 12 + .../Requests/Lockers/AddLockerRequest.cs | 24 + .../GetAllLockersPaginatedQueryParameters.cs | 19 +- .../Requests/Lockers/UpdateLockerRequest.cs | 12 + .../Payload/Requests/LoginModel.cs | 7 - .../Payload/Requests/RefreshTokenRequest.cs | 7 - .../Payload/Requests/Rooms/AddRoomRequest.cs | 24 + .../GetAllRoomsPaginatedQueryParameters.cs | 24 + ...EmptyContainersPaginatedQueryParameters.cs | 16 + .../Requests/Rooms/UpdateRoomRequest.cs | 20 + .../Requests/Staffs/AddStaffRequest.cs | 16 + .../GetAllStaffsPaginatedQueryParameters.cs | 18 + .../Staffs/RemoveStaffFromRoomRequest.cs | 6 - .../Requests/UpdateDepartmentRequest.cs | 6 - .../Payload/Requests/UpdateRoomRequest.cs | 8 - .../Payload/Requests/Users/AddUserRequest.cs | 40 + .../GetAllUsersPaginatedQueryParameters.cs | 21 + .../Requests/Users/UpdateUserRequest.cs | 17 +- .../Payload/Responses/LoginResult.cs | 1 + src/Api/Controllers/RoomsController.cs | 179 ++-- src/Api/Controllers/StaffsController.cs | 36 +- src/Api/Controllers/UsersController.cs | 79 +- .../Extensions/WebApplicationExtensions.cs | 16 +- src/Api/Program.cs | 8 +- src/Api/Services/CurrentUserService.cs | 1 - src/Api/appsettings.Testing.json | 20 + .../Common/Models/Dtos/DepartmentDto.cs | 6 +- .../Models/Dtos/Physical}/DocumentItemDto.cs | 5 +- .../Models/Dtos/Physical}/EmptyFolderDto.cs | 2 +- .../Models/Dtos/Physical}/EmptyLockerDto.cs | 3 +- .../Common/Models/Dtos/Physical/RoomDto.cs | 9 +- .../Common/Models/Dtos/Physical/StaffDto.cs | 8 +- src/Application/Common/Models/Dtos/UserDto.cs | 1 + .../Departments/Commands/AddDepartment.cs | 50 ++ .../AddDepartment/AddDepartmentCommand.cs | 43 - .../Departments/Commands/DeleteDepartment.cs | 41 + .../DeleteDepartmentCommand.cs | 38 - .../Departments/Commands/UpdateDepartment.cs | 14 + .../UpdateDepartmentCommand.cs | 10 - .../Departments/Queries/GetAllDepartments.cs | 32 + .../GetAllDepartmentsQuery.cs | 30 - .../Departments/Queries/GetDepartmentById.cs | 12 + .../GetDepartmentByIdQuery.cs | 9 - .../Documents/Commands/DeleteDocument.cs | 12 + .../DeleteDocument/DeleteDocumentCommand.cs | 9 - .../Documents/Commands/ImportDocument.cs | 76 ++ .../ImportDocument/ImportDocumentCommand.cs | 73 -- .../Documents/Commands/UpdateDocument.cs | 15 + .../UpdateDocument/UpdateDocumentCommand.cs | 12 - .../Documents/Queries/GetAllDocumentTypes.cs | 26 + .../Queries/GetAllDocumentsPaginated.cs | 138 +++ .../GetAllDocumentsPaginatedQuery.cs | 109 --- .../GetAllDocumentsPaginatedQueryValidator.cs | 27 - .../Documents/Queries/GetDocumentById.cs | 44 + .../GetDocumentById/GetDocumentByIdQuery.cs | 41 - .../GetAllDocumentTypesQuery.cs | 23 - .../GetDocumentTypes/GetDocumentTypesQuery.cs | 23 - src/Application/Folders/Commands/AddFolder.cs | 95 ++ .../Commands/AddFolder/AddFolderCommand.cs | 68 -- .../AddFolder/AddFolderCommandValidator.cs | 27 - .../Folders/Commands/DisableFolder.cs | 66 ++ .../DisableFolder/DisableFolderCommand.cs | 51 -- .../DisableFolderCommandValidator.cs | 14 - .../Folders/Commands/EnableFolder.cs | 12 + .../EnableFolder/EnableFolderCommand.cs | 9 - .../Folders/Commands/RemoveFolder.cs | 12 + .../RemoveFolder/RemoveFolderCommand.cs | 9 - .../Folders/Commands/UpdateFolder.cs | 15 + .../UpdateFolder/UpdateFolderCommand.cs | 12 - .../Folders/Queries/GetAllFoldersPaginated.cs | 18 + .../GetAllFoldersPaginatedQuery.cs | 15 - .../Folders/Queries/GetFolderById.cs | 12 + .../GetFolderById/GetFolderByIdQuery.cs | 9 - src/Application/Identity/IdentityData.cs | 7 +- src/Application/Lockers/Commands/AddLocker.cs | 96 +++ .../Commands/AddLocker/AddLockerCommand.cs | 69 -- .../AddLocker/AddLockerCommandValidator.cs | 25 - .../Lockers/Commands/DisableLocker.cs | 78 ++ .../DisableLocker/DisableLockerCommand.cs | 63 -- .../DisableLockerCommandValidator.cs | 13 - .../Lockers/Commands/EnableLocker.cs | 60 ++ .../EnableLocker/EnableLockerCommand.cs | 46 - .../EnableLockerCommandValidator.cs | 13 - .../Lockers/Commands/RemoveLocker.cs | 12 + .../RemoveLocker/RemoveLockerCommand.cs | 9 - .../Lockers/Commands/UpdateLocker.cs | 15 + .../UpdateLocker/UpdateLockerCommand.cs | 12 - .../Lockers/Queries/GetAllLockersPaginated.cs | 17 + .../GetAllLockersPaginatedQuery.cs | 14 - .../Lockers/Queries/GetLockerById.cs | 12 + .../GetLockerById/GetLockerByIdQuery.cs | 9 - src/Application/Rooms/Commands/AddRoom.cs | 91 ++ .../Rooms/Commands/AddRoom/AddRoomCommand.cs | 51 -- .../AddRoom/AddRoomCommandValidator.cs | 32 - src/Application/Rooms/Commands/DisableRoom.cs | 84 ++ .../DisableRoom/DisableRoomCommand.cs | 69 -- .../DisableRoomCommandValidator.cs | 14 - src/Application/Rooms/Commands/EnableRoom.cs | 12 + .../Commands/EnableRoom/EnableRoomCommand.cs | 9 - src/Application/Rooms/Commands/RemoveRoom.cs | 63 ++ .../Commands/RemoveRoom/RemoveRoomCommand.cs | 49 -- .../RemoveRoom/RemoveRoomCommandValidator.cs | 14 - src/Application/Rooms/Commands/UpdateRoom.cs | 15 + .../Commands/UpdateRoom/UpdateRoomCommand.cs | 12 - .../GetAllRoomsPaginatedQuery.cs | 13 - .../Rooms/Queries/GetAllRoomsPaginated.cs | 16 + .../Queries/GetEmptyContainersPaginated.cs | 54 ++ .../GetEmptyContainersPaginatedQuery.cs | 48 -- src/Application/Rooms/Queries/GetRoomById.cs | 12 + .../Queries/GetRoomById/GetRoomByIdQuery.cs | 9 - src/Application/Staffs/Commands/AddStaff.cs | 51 ++ .../Commands/AddStaff/AddStaffCommand.cs | 52 -- .../Staffs/Commands/RemoveStaffFromRoom.cs | 12 + .../RemoveStaffFromRoomCommand.cs | 10 - .../Staffs/Queries/GetAllStaffsPaginated.cs | 17 + .../GetAllStaffsPaginatedQuery.cs | 14 - .../Staffs/Queries/GetStaffById.cs | 12 + .../Queries/GetStaffById/GetStaffByIdQuery.cs | 9 - .../Staffs/Queries/GetStaffByRoom.cs | 12 + .../GetStaffByRoom/GetStaffByRoomQuery.cs | 9 - src/Application/Users/Commands/AddUser.cs | 118 +++ .../Users/Commands/AddUser/AddUserCommand.cs | 73 -- .../AddUser/AddUserCommandValidator.cs | 43 - src/Application/Users/Commands/DisableUser.cs | 47 + .../DisableUser/DisableUserCommand.cs | 44 - src/Application/Users/Commands/EnableUser.cs | 12 + .../Commands/EnableUser/EnableUserCommand.cs | 9 - src/Application/Users/Commands/UpdateUser.cs | 16 + .../Commands/UpdateUser/UpdateUserCommand.cs | 15 - .../Users/Queries/GetAllUsersPaginated.cs | 17 + .../GetAllUsersPaginatedQuery.cs | 14 - src/Application/Users/Queries/GetUserById.cs | 11 + .../Queries/GetUserById/GetUserByIdQuery.cs | 8 - .../GetUsersByName/GetUsersByNameQuery.cs | 40 - src/Domain/Entities/Department.cs | 2 + src/Domain/Entities/Physical/Room.cs | 2 + src/Domain/Entities/Physical/Staff.cs | 2 +- src/Domain/Entities/User.cs | 2 +- src/Infrastructure/Infrastructure.csproj | 4 - .../Configurations/DepartmentConfiguration.cs | 1 + .../Configurations/RoomConfiguration.cs | 1 + .../Configurations/StaffConfiguration.cs | 2 +- .../Configurations/UserConfiguration.cs | 2 +- ...00000000007_Add_Refresh_Token.Designer.cs} | 0 ...cs => 00000000000007_Add_Refresh_Token.cs} | 0 ...dRoomAndDepartmentRelationship.Designer.cs | 475 ++++++++++ ...ionshipAndRoomAndDepartmentRelationship.cs | 123 +++ ...000009_RoomMustHaveADepartment.Designer.cs | 477 ++++++++++ .../00000000000009_RoomMustHaveADepartment.cs | 22 + .../ApplicationDbContextModelSnapshot.cs | 29 +- .../BaseClassFixture.cs | 66 +- .../CustomApiFactory.cs | 2 +- .../Commands/AddDepartmentTests.cs | 20 +- .../Queries/GetAllDepartmentsTests.cs | 12 +- .../Queries/GetAllDocumentTypesTests.cs | 15 +- .../Queries/GetAllDocumentsPaginatedTests.cs | 814 +++++++++--------- .../Folders/Commands/AddFolderTests.cs | 234 ++--- .../Folders/Commands/DisableFolderTests.cs | 10 +- .../Lockers/Commands/AddLockerTests.cs | 17 +- .../Lockers/Commands/DisableLockerTests.cs | 66 +- .../Lockers/Commands/EnableLockerTests.cs | 80 +- .../Rooms/Commands/DisableRoomTests.cs | 26 +- .../Rooms/Commands/RemoveRoomTests.cs | 24 +- .../GetEmptyContainersPaginatedTests.cs | 126 +-- .../Users/Commands/AddUserTests.cs | 43 +- .../Common/Mappings/MappingTests.cs | 2 - 183 files changed, 4383 insertions(+), 2826 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Auth/LoginModel.cs create mode 100644 src/Api/Controllers/Payload/Requests/Auth/RefreshTokenRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Departments/AddDepartmentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Departments/UpdateDepartmentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Folders/AddFolderRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs delete mode 100644 src/Api/Controllers/Payload/Requests/LoginModel.cs delete mode 100644 src/Api/Controllers/Payload/Requests/RefreshTokenRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Rooms/AddRoomRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Rooms/GetEmptyContainersPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs delete mode 100644 src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs delete mode 100644 src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs delete mode 100644 src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs create mode 100644 src/Api/appsettings.Testing.json rename src/Application/{Documents/Queries/GetAllDocumentsPaginated => Common/Models/Dtos/Physical}/DocumentItemDto.cs (76%) rename src/Application/{Rooms/Queries/GetEmptyContainersPaginated => Common/Models/Dtos/Physical}/EmptyFolderDto.cs (89%) rename src/Application/{Rooms/Queries/GetEmptyContainersPaginated => Common/Models/Dtos/Physical}/EmptyLockerDto.cs (86%) create mode 100644 src/Application/Departments/Commands/AddDepartment.cs delete mode 100644 src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs create mode 100644 src/Application/Departments/Commands/DeleteDepartment.cs delete mode 100644 src/Application/Departments/Commands/DeleteDepartment/DeleteDepartmentCommand.cs create mode 100644 src/Application/Departments/Commands/UpdateDepartment.cs delete mode 100644 src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs create mode 100644 src/Application/Departments/Queries/GetAllDepartments.cs delete mode 100644 src/Application/Departments/Queries/GetAllDepartments/GetAllDepartmentsQuery.cs create mode 100644 src/Application/Departments/Queries/GetDepartmentById.cs delete mode 100644 src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs create mode 100644 src/Application/Documents/Commands/DeleteDocument.cs delete mode 100644 src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs create mode 100644 src/Application/Documents/Commands/ImportDocument.cs delete mode 100644 src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs create mode 100644 src/Application/Documents/Commands/UpdateDocument.cs delete mode 100644 src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentTypes.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentsPaginated.cs delete mode 100644 src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQuery.cs delete mode 100644 src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQueryValidator.cs create mode 100644 src/Application/Documents/Queries/GetDocumentById.cs delete mode 100644 src/Application/Documents/Queries/GetDocumentById/GetDocumentByIdQuery.cs delete mode 100644 src/Application/Documents/Queries/GetDocumentTypes/GetAllDocumentTypesQuery.cs delete mode 100644 src/Application/Documents/Queries/GetDocumentTypes/GetDocumentTypesQuery.cs create mode 100644 src/Application/Folders/Commands/AddFolder.cs delete mode 100644 src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs delete mode 100644 src/Application/Folders/Commands/AddFolder/AddFolderCommandValidator.cs create mode 100644 src/Application/Folders/Commands/DisableFolder.cs delete mode 100644 src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs delete mode 100644 src/Application/Folders/Commands/DisableFolder/DisableFolderCommandValidator.cs create mode 100644 src/Application/Folders/Commands/EnableFolder.cs delete mode 100644 src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs create mode 100644 src/Application/Folders/Commands/RemoveFolder.cs delete mode 100644 src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs create mode 100644 src/Application/Folders/Commands/UpdateFolder.cs delete mode 100644 src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs create mode 100644 src/Application/Folders/Queries/GetAllFoldersPaginated.cs delete mode 100644 src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs create mode 100644 src/Application/Folders/Queries/GetFolderById.cs delete mode 100644 src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs create mode 100644 src/Application/Lockers/Commands/AddLocker.cs delete mode 100644 src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs delete mode 100644 src/Application/Lockers/Commands/AddLocker/AddLockerCommandValidator.cs create mode 100644 src/Application/Lockers/Commands/DisableLocker.cs delete mode 100644 src/Application/Lockers/Commands/DisableLocker/DisableLockerCommand.cs delete mode 100644 src/Application/Lockers/Commands/DisableLocker/DisableLockerCommandValidator.cs create mode 100644 src/Application/Lockers/Commands/EnableLocker.cs delete mode 100644 src/Application/Lockers/Commands/EnableLocker/EnableLockerCommand.cs delete mode 100644 src/Application/Lockers/Commands/EnableLocker/EnableLockerCommandValidator.cs create mode 100644 src/Application/Lockers/Commands/RemoveLocker.cs delete mode 100644 src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs create mode 100644 src/Application/Lockers/Commands/UpdateLocker.cs delete mode 100644 src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs create mode 100644 src/Application/Lockers/Queries/GetAllLockersPaginated.cs delete mode 100644 src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs create mode 100644 src/Application/Lockers/Queries/GetLockerById.cs delete mode 100644 src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs create mode 100644 src/Application/Rooms/Commands/AddRoom.cs delete mode 100644 src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs delete mode 100644 src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs create mode 100644 src/Application/Rooms/Commands/DisableRoom.cs delete mode 100644 src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs delete mode 100644 src/Application/Rooms/Commands/DisableRoom/DisableRoomCommandValidator.cs create mode 100644 src/Application/Rooms/Commands/EnableRoom.cs delete mode 100644 src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs create mode 100644 src/Application/Rooms/Commands/RemoveRoom.cs delete mode 100644 src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommand.cs delete mode 100644 src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommandValidator.cs create mode 100644 src/Application/Rooms/Commands/UpdateRoom.cs delete mode 100644 src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs delete mode 100644 src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs create mode 100644 src/Application/Rooms/Queries/GetAllRoomsPaginated.cs create mode 100644 src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs delete mode 100644 src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs create mode 100644 src/Application/Rooms/Queries/GetRoomById.cs delete mode 100644 src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs create mode 100644 src/Application/Staffs/Commands/AddStaff.cs delete mode 100644 src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs create mode 100644 src/Application/Staffs/Commands/RemoveStaffFromRoom.cs delete mode 100644 src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs create mode 100644 src/Application/Staffs/Queries/GetAllStaffsPaginated.cs delete mode 100644 src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs create mode 100644 src/Application/Staffs/Queries/GetStaffById.cs delete mode 100644 src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs create mode 100644 src/Application/Staffs/Queries/GetStaffByRoom.cs delete mode 100644 src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs create mode 100644 src/Application/Users/Commands/AddUser.cs delete mode 100644 src/Application/Users/Commands/AddUser/AddUserCommand.cs delete mode 100644 src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs create mode 100644 src/Application/Users/Commands/DisableUser.cs delete mode 100644 src/Application/Users/Commands/DisableUser/DisableUserCommand.cs create mode 100644 src/Application/Users/Commands/EnableUser.cs delete mode 100644 src/Application/Users/Commands/EnableUser/EnableUserCommand.cs create mode 100644 src/Application/Users/Commands/UpdateUser.cs delete mode 100644 src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs create mode 100644 src/Application/Users/Queries/GetAllUsersPaginated.cs delete mode 100644 src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs create mode 100644 src/Application/Users/Queries/GetUserById.cs delete mode 100644 src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs delete mode 100644 src/Application/Users/Queries/GetUsersByName/GetUsersByNameQuery.cs rename src/Infrastructure/Persistence/Migrations/{20230524080300_Add_Refresh_Token.Designer.cs => 00000000000007_Add_Refresh_Token.Designer.cs} (100%) rename src/Infrastructure/Persistence/Migrations/{20230524080300_Add_Refresh_Token.cs => 00000000000007_Add_Refresh_Token.cs} (100%) create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d7d44b92..993b5ad1 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -8,7 +8,7 @@ services: ports: - '8888:80' environment: - - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_ENVIRONMENT=Testing - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured; depends_on: database: diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 09720f26..11e16ecd 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + true diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 6dea0c7d..39dabb10 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -1,6 +1,8 @@ +using System.Reflection; using Api.Middlewares; using Api.Policies; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.OpenApi.Models; namespace Api; @@ -31,7 +33,18 @@ public static IServiceCollection AddApiServices(this IServiceCollection services // For swagger services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(); + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "ProFile API", + Description = "An ASP.NET Core Web API for managing documents", + }); + + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); return services; } diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index efdd1591..a770a036 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -1,6 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Authentication; -using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Auth; using Api.Controllers.Payload.Responses; using Application.Common.Interfaces; using Application.Common.Models; @@ -23,6 +23,11 @@ public AuthController(IIdentityService identityService) _identityService = identityService; } + /// + /// Login + /// + /// Login credentials + /// A LoginResult indicating the result of logging in [AllowAnonymous] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] @@ -48,23 +53,11 @@ public async Task>> Login([FromBody] LoginModel return Ok(Result.Succeed(loginResult)); } - - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task Logout() - { - var refreshToken = Request.Cookies[nameof(RefreshToken)]; - var jweToken = Request.Cookies["JweToken"]; - - RemoveJweToken(); - RemoveRefreshToken(); - - await _identityService.LogoutAsync(jweToken!, refreshToken!); - - return Ok(); - } - + + /// + /// Refresh session and token + /// + /// An IActionResult indicating the result of refreshing token [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -90,6 +83,10 @@ public async Task Refresh() return Ok(); } + /// + /// Validate current user + /// + /// An IActionResult indicating the result of validating the user [Authorize] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] @@ -99,11 +96,32 @@ public IActionResult Validate() return Ok(); } + /// + /// Logout of the system + /// + /// An IActionResult indicating the result of logging out of the system + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Logout() + { + var refreshToken = Request.Cookies[nameof(RefreshToken)]; + var jweToken = Request.Cookies["JweToken"]; + + RemoveJweToken(); + RemoveRefreshToken(); + + await _identityService.LogoutAsync(jweToken!, refreshToken!); + + return Ok(); + } + private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken) { var cookieOptions = new CookieOptions { HttpOnly = true, + Expires = newRefreshToken.ExpiryDateTime }; var handler = new JwtSecurityTokenHandler(); Response.Cookies.Append("JweToken", handler.WriteToken(jweToken), cookieOptions); diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 98045cf7..bf01c28a 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -1,10 +1,8 @@ -using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Departments; using Application.Common.Models; -using Application.Departments.Commands.DeleteDepartment; -using Application.Departments.Commands.UpdateDepartment; -using Application.Departments.Commands.AddDepartment; -using Application.Departments.Queries.GetAllDepartments; -using Application.Departments.Queries.GetDepartmentById; +using Application.Common.Models.Dtos; +using Application.Departments.Commands; +using Application.Departments.Queries; using Application.Identity; using Application.Users.Queries; using Infrastructure.Identity.Authorization; @@ -15,52 +13,57 @@ namespace Api.Controllers; public class DepartmentsController : ApiControllerBase { /// - /// Create a department + /// Get back a department based on its id /// - /// command parameter to create a department - /// Result[DepartmentDto] - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost] + /// id of the department to be retrieved + /// A DepartmentDto of the retrieved department + [HttpGet("{departmentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddDepartment([FromBody] AddDepartmentCommand command) + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid departmentId) { - var result = await Mediator.Send(command); + var query = new GetDepartmentById.Query() + { + DepartmentId = departmentId + }; + var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + /// /// Get all documents /// - /// a Result of an IEnumerable of DepartmentDto + /// A list of DocumentDto [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllDepartments() + public async Task>>> GetAll() { - var result = await Mediator.Send(new GetAllDepartmentsQuery()); + var result = await Mediator.Send(new GetAllDepartments.Query()); return Ok(Result>.Succeed(result)); } /// - /// Get back a department based on its id + /// Add a department /// - /// id of the department to be retrieved - /// A DepartmentDto of the retrieved department - [HttpGet("{departmentId:guid}")] + /// Add department details + /// A DepartmentDto of the the added department + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>> GetDepartmentById([FromRoute] Guid departmentId) + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Add([FromBody] AddDepartmentRequest request) { - var query = new GetDepartmentByIdQuery() + var command = new AddDepartment.Command() { - DepartmentId = departmentId + Name = request.Name, }; - var result = await Mediator.Send(query); + var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// /// Update a department /// @@ -72,9 +75,9 @@ public async Task>> GetDepartmentById([FromRo [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> UpdateDepartment([FromRoute] Guid departmentId, [FromBody] UpdateDepartmentRequest request) + public async Task>> Update([FromRoute] Guid departmentId, [FromBody] UpdateDepartmentRequest request) { - var command = new UpdateDepartmentCommand() + var command = new UpdateDepartment.Command() { DepartmentId = departmentId, Name = request.Name @@ -93,9 +96,9 @@ public async Task>> UpdateDepartment([FromRou [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> DeleteDepartment([FromRoute] Guid departmentId) + public async Task>> Delete([FromRoute] Guid departmentId) { - var command = new DeleteDepartmentCommand() + var command = new DeleteDepartment.Command() { DepartmentId = departmentId, }; diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 41f8fb3d..5c56986b 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,12 +1,8 @@ using Api.Controllers.Payload.Requests.Documents; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; -using Application.Documents.Commands.DeleteDocument; -using Application.Documents.Commands.ImportDocument; -using Application.Documents.Commands.UpdateDocument; -using Application.Documents.Queries.GetAllDocumentsPaginated; -using Application.Documents.Queries.GetDocumentById; -using Application.Documents.Queries.GetDocumentTypes; +using Application.Documents.Commands; +using Application.Documents.Queries; using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -15,65 +11,94 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPost] + /// + /// Get a document by id + /// + /// Id of the document to be retrieved + /// A DocumentDto of the retrieved document + [HttpGet("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> ImportDocument([FromBody] ImportDocumentCommand command) + public async Task>> GetById([FromRoute] Guid documentId) { - var result = await Mediator.Send(command); + var query = new GetDocumentById.Query() + { + DocumentId = documentId, + }; + var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("types")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllDocumentTypes() - { - var result = await Mediator.Send(new GetAllDocumentTypesQuery()); - return Ok(Result>.Succeed(result)); - } - + /// + /// Get all documents paginated + /// + /// Get all documents query parameters + /// A paginated list of DocumentDto [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllDocuments(Guid? roomId, Guid? lockerId, Guid? folderId, int? page, int? size, string? sortBy, string? sortOrder) + public async Task>>> GetAllPaginated( + [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { - var query = new GetAllDocumentsPaginatedQuery() + var query = new GetAllDocumentsPaginated.Query() { - RoomId = roomId, - LockerId = lockerId, - FolderId = folderId, - Page = page, - Size = size, - SortBy = sortBy, - SortOrder = sortOrder + RoomId = queryParameters.RoomId, + LockerId = queryParameters.LockerId, + FolderId = queryParameters.FolderId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - [HttpGet("{id:guid}")] + /// + /// Get all document types + /// + /// A list of document types + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet("types")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllDocumentTypes() + { + var result = await Mediator.Send(new GetAllDocumentTypes.Query()); + return Ok(Result>.Succeed(result)); + } + + /// + /// Import a document + /// + /// Import document details + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetDocumentById(Guid id) + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Import([FromBody] ImportDocumentRequest request) { - var query = new GetDocumentByIdQuery() + var command = new ImportDocument.Command() { - Id = id + Title = request.Title, + Description = request.Description, + DocumentType = request.DocumentType, + FolderId = request.FolderId, + ImporterId = request.ImporterId, }; - var result = await Mediator.Send(query); + var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// /// Update a document /// @@ -86,9 +111,9 @@ public async Task>> GetDocumentById(Guid id) [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> UpdateDocument([FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) + public async Task>> Update([FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) { - var query = new UpdateDocumentCommand() + var query = new UpdateDocument.Command() { DocumentId = documentId, Title = request.Title, @@ -98,7 +123,7 @@ public async Task>> UpdateDocument([FromRoute] var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + /// /// Delete a document /// @@ -108,9 +133,9 @@ public async Task>> UpdateDocument([FromRoute] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> UpdateDocument([FromRoute] Guid documentId) + public async Task>> Delete([FromRoute] Guid documentId) { - var query = new DeleteDocumentCommand() + var query = new DeleteDocument.Command() { DocumentId = documentId, }; diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 8ea03dd6..3365ec81 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,13 +1,8 @@ using Api.Controllers.Payload.Requests.Folders; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; -using Application.Folders.Commands.AddFolder; -using Application.Folders.Commands.DisableFolder; -using Application.Folders.Commands.EnableFolder; -using Application.Folders.Commands.RemoveFolder; -using Application.Folders.Commands.UpdateFolder; -using Application.Folders.Queries.GetAllFoldersPaginated; -using Application.Folders.Queries.GetFolderById; +using Application.Folders.Commands; +using Application.Folders.Queries; using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -28,9 +23,9 @@ public class FoldersController : ApiControllerBase [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid folderId) { - var query = new GetFolderByIdQuery() + var query = new GetFolderById.Query() { - FolderId = folderId + FolderId = folderId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -39,7 +34,7 @@ public async Task>> GetById([FromRoute] Guid fold /// /// Get all folders paginated /// - /// Get all folders query parameters + /// Get all folders paginated query parameters /// A paginated list of FolderDto [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] @@ -48,7 +43,7 @@ public async Task>> GetById([FromRoute] Guid fold public async Task>>> GetAllPaginated( [FromQuery] GetAllFoldersPaginatedQueryParameters queryParameters) { - var query = new GetAllFoldersPaginatedQuery() + var query = new GetAllFoldersPaginated.Query() { RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, @@ -64,7 +59,7 @@ public async Task>>> GetAllPaginate /// /// Add a folder /// - /// Add folder details + /// Add folder details /// A FolderDto of the added folder [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] @@ -73,8 +68,15 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddFolder([FromBody] AddFolderCommand command) + public async Task>> AddFolder([FromBody] AddFolderRequest request) { + var command = new AddFolder.Command() + { + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, + LockerId = request.LockerId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -93,7 +95,7 @@ public async Task>> AddFolder([FromBody] AddFolde [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveFolder([FromRoute] Guid folderId) { - var command = new RemoveFolderCommand() + var command = new RemoveFolder.Command() { FolderId = folderId, }; @@ -104,17 +106,21 @@ public async Task>> RemoveFolder([FromRoute] Guid /// /// Enable a folder /// - /// Enable folder details + /// Id of the folder to be enabled /// A FolderDto of the enabled folder [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable")] - [ProducesResponseType(StatusCodes.Status200OK)] + [HttpPut("enable/{folderId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableFolder([FromBody] EnableFolderCommand command) + public async Task>> EnableFolder([FromRoute] Guid folderId) { + var command = new EnableFolder.Command() + { + FolderId = folderId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -122,17 +128,21 @@ public async Task>> EnableFolder([FromBody] Enabl /// /// Disable a folder /// - /// Disable folder details + /// Id of the disabled folder /// A FolderDto of the disabled folder [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable")] + [HttpPut("disable/{folderId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableFolder([FromBody] DisableFolderCommand command) + public async Task>> DisableFolder([FromRoute] Guid folderId) { + var command = new DisableFolder.Command() + { + FolderId = folderId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -150,12 +160,12 @@ public async Task>> DisableFolder([FromBody] Disa [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) { - var command = new UpdateFolderCommand() + var command = new UpdateFolder.Command() { FolderId = folderId, Name = request.Name, Description = request.Description, - Capacity = request.Capacity + Capacity = request.Capacity, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index f064090e..373a0cee 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,15 +1,9 @@ -using Api.Controllers.Payload.Requests; -using Api.Controllers.Payload.Requests.Lockers; +using Api.Controllers.Payload.Requests.Lockers; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; -using Application.Lockers.Commands.AddLocker; -using Application.Lockers.Commands.DisableLocker; -using Application.Lockers.Commands.EnableLocker; -using Application.Lockers.Commands.RemoveLocker; -using Application.Lockers.Commands.UpdateLocker; -using Application.Lockers.Queries.GetAllLockersPaginated; -using Application.Lockers.Queries.GetLockerById; +using Application.Lockers.Commands; +using Application.Lockers.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -28,9 +22,9 @@ public class LockersController : ApiControllerBase [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid lockerId) { - var query = new GetLockerByIdQuery() + var query = new GetLockerById.Query() { - LockerId = lockerId + LockerId = lockerId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -39,7 +33,7 @@ public async Task>> GetById([FromRoute] Guid lock /// /// Get all lockers paginated /// - /// Get all lockers query parameters + /// Get all lockers paginated query parameters /// A paginated list of LockerDto [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] @@ -47,7 +41,7 @@ public async Task>> GetById([FromRoute] Guid lock public async Task>>> GetAllPaginated( [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) { - var query = new GetAllLockersPaginatedQuery() + var query = new GetAllLockersPaginated.Query() { RoomId = queryParameters.RoomId, Page = queryParameters.Page, @@ -59,84 +53,114 @@ public async Task>>> GetAllPaginate return Ok(Result>.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Staff)] + /// + /// Add a locker + /// + /// Add locker details + /// A LockerDto of the added locker + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddLocker([FromBody] AddLockerCommand command) + public async Task>> Add([FromBody] AddLockerRequest request) { + var command = new AddLocker.Command() + { + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, + RoomId = request.RoomId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable")] + /// + /// Remove a locker + /// + /// Id of the locker to be removed + /// A LockerDto of the removed locker + [HttpDelete("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableLocker([FromBody] DisableLockerCommand command) + public async Task>> Remove([FromRoute] Guid lockerId) { + var command = new RemoveLocker.Command() + { + LockerId = lockerId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + /// + /// Enable a locker + /// + /// Id of the locker to be enabled + /// A LockerDto of the enabled locker [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable")] + [HttpPut("enable/{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableLocker([FromBody] EnableLockerCommand command) + public async Task>> Enable([FromRoute] Guid lockerId) { + var command = new EnableLocker.Command() + { + LockerId = lockerId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } /// - /// Update a locker + /// Disable a locker /// - /// Id of the locker to be updated - /// Update locker details - /// A LockerDto of the updated locker - [HttpPut("{lockerId:guid}")] + /// Id of the locker to be disabled + /// A LockerDto of the disabled locker + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpPut("disable/{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) + public async Task>> Disable([FromRoute] Guid lockerId) { - var command = new UpdateLockerCommand() + var command = new DisableLocker.Command() { LockerId = lockerId, - Name = request.Name, - Description = request.Description, - Capacity = request.Capacity }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// - /// Remove a locker + /// Update a locker /// - /// Id of the locker to be removed - /// A LockerDto of the removed locker - [HttpDelete("{lockerId:guid}")] + /// Id of the locker to be updated + /// Update locker details + /// A LockerDto of the updated locker + [HttpPut("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Remove([FromRoute] Guid lockerId) + public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) { - var command = new RemoveLockerCommand() + var command = new UpdateLocker.Command() { - LockerId = lockerId + LockerId = lockerId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Auth/LoginModel.cs b/src/Api/Controllers/Payload/Requests/Auth/LoginModel.cs new file mode 100644 index 00000000..99f49b03 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Auth/LoginModel.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Auth; + +/// +/// Login credentials to login +/// +public class LoginModel +{ + /// + /// Email of user + /// + public string Email { get; set; } = null!; + /// + /// Password of user + /// + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Auth/RefreshTokenRequest.cs b/src/Api/Controllers/Payload/Requests/Auth/RefreshTokenRequest.cs new file mode 100644 index 00000000..979dc447 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Auth/RefreshTokenRequest.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Auth; + +/// +/// Request details to refresh token +/// +public class RefreshTokenRequest +{ + /// + /// Access token + /// + public string Token { get; set; } = null!; + /// + /// Refresh token + /// + public string RefreshToken { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Departments/AddDepartmentRequest.cs b/src/Api/Controllers/Payload/Requests/Departments/AddDepartmentRequest.cs new file mode 100644 index 00000000..2f6444f7 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Departments/AddDepartmentRequest.cs @@ -0,0 +1,12 @@ +namespace Api.Controllers.Payload.Requests.Departments; + +/// +/// Request details to add a department +/// +public class AddDepartmentRequest +{ + /// + /// Name of the department to be added + /// + public string Name { get; init; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Departments/UpdateDepartmentRequest.cs b/src/Api/Controllers/Payload/Requests/Departments/UpdateDepartmentRequest.cs new file mode 100644 index 00000000..48ad1052 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Departments/UpdateDepartmentRequest.cs @@ -0,0 +1,12 @@ +namespace Api.Controllers.Payload.Requests.Departments; + +/// +/// Request details to update a department +/// +public class UpdateDepartmentRequest +{ + /// + /// New name of the department to be updated + /// + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs new file mode 100644 index 00000000..7f134c04 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -0,0 +1,40 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Query parameters for getting all documents with pagination +/// +public class GetAllDocumentsPaginatedQueryParameters +{ + /// + /// Id of the room to find documents in + /// + public Guid? RoomId { get; set; } + /// + /// Id of the locker to find documents in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to find documents in + /// + public Guid? FolderId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } + /// + /// Sort criteria + /// + public string? SortBy { get; set; } + /// + /// Sort direction + /// + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs new file mode 100644 index 00000000..0bbb2723 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs @@ -0,0 +1,28 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Request details to import a document +/// +public class ImportDocumentRequest +{ + /// + /// Title of the document to be imported + /// + public string Title { get; set; } = null!; + /// + /// Description of the document to be imported + /// + public string? Description { get; set; } + /// + /// Document type of the document to be imported + /// + public string DocumentType { get; set; } = null!; + /// + /// Id of the importer + /// + public Guid ImporterId { get; set; } + /// + /// Id of the folder that this document will be in + /// + public Guid FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs index e6a0885b..a31cec88 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs @@ -1,8 +1,20 @@ namespace Api.Controllers.Payload.Requests.Documents; +/// +/// Request details to update a document +/// public class UpdateDocumentRequest { + /// + /// New title of the document to be updated + /// public string Title { get; set; } = null!; + /// + /// New description of the document to be updated + /// public string? Description { get; set; } + /// + /// New document type of the document to be updated + /// public string DocumentType { get; set; } = null!; } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/AddFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Folders/AddFolderRequest.cs new file mode 100644 index 00000000..1dd731c0 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Folders/AddFolderRequest.cs @@ -0,0 +1,23 @@ +namespace Api.Controllers.Payload.Requests.Folders; +/// +/// Request details to add a folder +/// +public class AddFolderRequest +{ + /// + /// Name of the folder to be added + /// + public string Name { get; init; } = null!; + /// + /// Description of the folder to be added + /// + public string? Description { get; init; } + /// + /// Number of documents this folder can hold + /// + public int Capacity { get; init; } + /// + /// Id of the locker that this folder will be in + /// + public Guid LockerId { get; init; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index db642122..1be3d84f 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -1,11 +1,32 @@ namespace Api.Controllers.Payload.Requests.Folders; +/// +/// Query parameters for getting all folders with pagination +/// public class GetAllFoldersPaginatedQueryParameters { + /// + /// Id of the room to find folders in + /// public Guid? RoomId { get; set; } + /// + /// Id of the locker to find folders in + /// public Guid? LockerId { get; set; } + /// + /// Page number + /// public int? Page { get; set; } + /// + /// Size number + /// public int? Size { get; set; } + /// + /// Sort criteria + /// public string? SortBy { get; set; } + /// + /// Sort direction + /// public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs index 97e9691f..e26cf33b 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs @@ -1,8 +1,20 @@ namespace Api.Controllers.Payload.Requests.Folders; +/// +/// Request details to update a folder +/// public class UpdateFolderRequest { + /// + /// New name of the folder to be updated + /// public string Name { get; set; } = null!; + /// + /// New description of the folder to be updated + /// public string? Description { get; set; } + /// + /// New capacity of the folder to be updated + /// public int Capacity { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs new file mode 100644 index 00000000..67988e72 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Lockers; + +/// +/// Request details to add a locker +/// +public class AddLockerRequest +{ + /// + /// Name of the locker to be updated + /// + public string Name { get; init; } = null!; + /// + /// Description of the locker to be updated + /// + public string? Description { get; init; } + /// + /// Id of the room that this locker will be in + /// + public Guid RoomId { get; init; } + /// + /// Number of folders this locker can hold + /// + public int Capacity { get; init; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index b4b7f837..656b96f6 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -2,12 +2,29 @@ namespace Api.Controllers.Payload.Requests.Lockers; - +/// +/// Query parameters for getting all lockers with pagination +/// public class GetAllLockersPaginatedQueryParameters { + /// + /// Id of the room to find lockers in + /// public Guid? RoomId { get; set; } + /// + /// Page number + /// public int? Page { get; set; } + /// + /// Size number + /// public int? Size { get; set; } + /// + /// Sort criteria + /// public string? SortBy { get; set; } + /// + /// Sort direction + /// public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs index a7f69036..5558c1ce 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs @@ -1,8 +1,20 @@ namespace Api.Controllers.Payload.Requests.Lockers; +/// +/// Request details to update a locker +/// public class UpdateLockerRequest { + /// + /// New name of the locker to be updated + /// public string Name { get; set; } = null!; + /// + /// New description of the locker to be updated + /// public string? Description { get; set; } + /// + /// New capacity of the locker to be updated + /// public int Capacity { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/LoginModel.cs b/src/Api/Controllers/Payload/Requests/LoginModel.cs deleted file mode 100644 index d5bd6c61..00000000 --- a/src/Api/Controllers/Payload/Requests/LoginModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Api.Controllers.Payload.Requests; - -public class LoginModel -{ - public string Email { get; set; } - public string Password { get; set; } -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/RefreshTokenRequest.cs b/src/Api/Controllers/Payload/Requests/RefreshTokenRequest.cs deleted file mode 100644 index bcc3d8e2..00000000 --- a/src/Api/Controllers/Payload/Requests/RefreshTokenRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Api.Controllers.Payload.Requests; - -public class RefreshTokenRequest -{ - public string Token { get; set; } - public string RefreshToken { get; set; } -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/AddRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Rooms/AddRoomRequest.cs new file mode 100644 index 00000000..4228f4da --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Rooms/AddRoomRequest.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Rooms; + +/// +/// Request details to add a room +/// +public class AddRoomRequest +{ + /// + /// Name of the room to be added + /// + public string Name { get; set; } = null!; + /// + /// Description of the room to be added + /// + public string? Description { get; set; } + /// + /// Number of lockers this room can hold + /// + public int Capacity { get; set; } + /// + /// Id of the department this room belongs to + /// + public Guid DepartmentId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs new file mode 100644 index 00000000..34721c39 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Rooms; + +/// +/// Query parameters for getting all rooms with pagination +/// +public class GetAllRoomsPaginatedQueryParameters +{ + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } + /// + /// Sort criteria + /// + public string? SortBy { get; set; } + /// + /// Sort direction + /// + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetEmptyContainersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetEmptyContainersPaginatedQueryParameters.cs new file mode 100644 index 00000000..2d60bb6d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetEmptyContainersPaginatedQueryParameters.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Rooms; + +/// +/// Query parameters for getting all empty containers in a room +/// +public class GetEmptyContainersPaginatedQueryParameters +{ + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs new file mode 100644 index 00000000..40d4965c --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs @@ -0,0 +1,20 @@ +namespace Api.Controllers.Payload.Requests.Rooms; + +/// +/// Request details to update a room +/// +public class UpdateRoomRequest +{ + /// + /// New name of the room to be updated + /// + public string Name { get; set; } = null!; + /// + /// New description of the room to be updated + /// + public string? Description { get; set; } + /// + /// New capacity of the room to be updated + /// + public int Capacity { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs new file mode 100644 index 00000000..47ebbc06 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Staffs; + +/// +/// Request details to add a staff +/// +public class AddStaffRequest +{ + /// + /// User id of the new staff + /// + public Guid UserId { get; init; } + /// + /// Id of the room this staff will be in + /// + public Guid? RoomId { get; init; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs index bcb47cb4..53fcbb87 100644 --- a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs @@ -1,10 +1,28 @@ namespace Api.Controllers.Payload.Requests.Staffs; +/// +/// Query parameters for getting all staffs with pagination +/// public class GetAllStaffsPaginatedQueryParameters { + /// + /// Search term + /// public string? SearchTerm { get; set; } + /// + /// Page number + /// public int? Page { get; set; } + /// + /// Size number + /// public int? Size { get; set; } + /// + /// Sort criteria + /// public string? SortBy { get; set; } + /// + /// Sort direction + /// public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs deleted file mode 100644 index 811608df..00000000 --- a/src/Api/Controllers/Payload/Requests/Staffs/RemoveStaffFromRoomRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Api.Controllers.Payload.Requests.Staffs; - -public class RemoveStaffFromRoomRequest -{ - public Guid RoomId { get; set; } -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs b/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs deleted file mode 100644 index 467ceeb5..00000000 --- a/src/Api/Controllers/Payload/Requests/UpdateDepartmentRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Api.Controllers.Payload.Requests; - -public class UpdateDepartmentRequest -{ - public string Name { get; set; } = null!; -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs b/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs deleted file mode 100644 index a579a447..00000000 --- a/src/Api/Controllers/Payload/Requests/UpdateRoomRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Api.Controllers.Payload.Requests; - -public class UpdateRoomRequest -{ - public string Name { get; set; } = null!; - public string? Description { get; set; } - public int Capacity { get; set; } -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs new file mode 100644 index 00000000..2c449aff --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs @@ -0,0 +1,40 @@ +namespace Api.Controllers.Payload.Requests.Users; + +/// +/// Request details to add a user +/// +public class AddUserRequest +{ + /// + /// Username of the user to be added + /// + public string Username { get; init; } = null!; + /// + /// Email of the user to be added + /// + public string Email { get; init; } = null!; + /// + /// Password of the user to be added + /// + public string Password { get; init; } = null!; + /// + /// First name of the user to be added + /// + public string? FirstName { get; init; } + /// + /// Last name of the user to be added + /// + public string? LastName { get; init; } + /// + /// Department of the user to be added + /// + public Guid DepartmentId { get; init; } + /// + /// Role of the user to be added + /// + public string Role { get; init; } = null!; + /// + /// Position of the user to be added + /// + public string? Position { get; init; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index d3484660..9f47d022 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -1,11 +1,32 @@ namespace Api.Controllers.Payload.Requests.Users; +/// +/// Query parameters for getting all users with pagination +/// public class GetAllUsersPaginatedQueryParameters { + /// + /// Id of the department to find users in + /// public Guid? DepartmentId { get; set; } + /// + /// Search term + /// public string? SearchTerm { get; set; } + /// + /// Page number + /// public int? Page { get; set; } + /// + /// Size number + /// public int? Size { get; set; } + /// + /// Sort criteria + /// public string? SortBy { get; set; } + /// + /// Sort direction + /// public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs index 77bcb0e4..d162c28a 100644 --- a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs @@ -1,11 +1,24 @@ namespace Api.Controllers.Payload.Requests.Users; +/// +/// Request details to update a user +/// public class UpdateUserRequest { - public string Username { get; set; } = null!; - public string Email { get; set; } = null!; + /// + /// New first name of the user to be updated + /// public string? FirstName { get; set; } + /// + /// New last name of the user to be updated + /// public string? LastName { get; set; } + /// + /// New role of the user to be updated + /// public string Role { get; set; } = null!; + /// + /// New position of the user to be updated + /// public string? Position { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Responses/LoginResult.cs b/src/Api/Controllers/Payload/Responses/LoginResult.cs index 29fb21c4..f731ac4a 100644 --- a/src/Api/Controllers/Payload/Responses/LoginResult.cs +++ b/src/Api/Controllers/Payload/Responses/LoginResult.cs @@ -1,3 +1,4 @@ +using Application.Common.Models.Dtos; using Application.Users.Queries; namespace Api.Controllers.Payload.Responses; diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 3cbeb8dd..d46390cc 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,15 +1,10 @@ -using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Lockers; +using Api.Controllers.Payload.Requests.Rooms; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; -using Application.Rooms.Commands.AddRoom; -using Application.Rooms.Commands.DisableRoom; -using Application.Rooms.Commands.EnableRoom; -using Application.Rooms.Commands.RemoveRoom; -using Application.Rooms.Commands.UpdateRoom; -using Application.Rooms.Queries.GetAllRoomPaginated; -using Application.Rooms.Queries.GetEmptyContainersPaginated; -using Application.Rooms.Queries.GetRoomById; +using Application.Rooms.Commands; +using Application.Rooms.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,135 +12,179 @@ namespace Api.Controllers; public class RoomsController : ApiControllerBase { - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost] + /// + /// Get a room by id + /// + /// Id of the room to be retrieved + /// A RoomDto of the retrieved room + [HttpGet("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddRoom(AddRoomCommand command) + public async Task>> GetById([FromRoute] Guid roomId) { - var result = await Mediator.Send(command); + var query = new GetRoomById.Query() + { + RoomId = roomId, + }; + var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + + /// + /// Get all rooms paginated + /// + /// Get all rooms paginated details + /// A paginated list of rooms + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) + { + var query = new GetAllRoomsPaginated.Query() + { + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get empty containers in a room + /// + /// Get empty containers paginated details + /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] [HttpPost("empty-containers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetEmptyContainers(GetEmptyContainersPaginatedQuery query) + public async Task>> GetEmptyContainers( + [FromRoute] Guid roomId, + [FromQuery] GetEmptyContainersPaginatedQueryParameters queryParameters) { + var query = new GetEmptyContainersPaginated.Query() + { + RoomId = roomId, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + + /// + /// Add a room + /// + /// Add room details + /// A RoomDto of the added room [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("disable")] + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableRoom(DisableRoomCommand command) + public async Task>> AddRoom([FromBody] AddRoomRequest request) { + var command = new AddRoom.Command() + { + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, + DepartmentId = request.DepartmentId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + + /// + /// Remove a room + /// + /// Id of the room to be removed + /// A RoomDto of the removed room [RequiresRole(IdentityData.Roles.Admin)] - [HttpDelete] + [HttpDelete("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RemoveRoom(RemoveRoomCommand command) + public async Task>> RemoveRoom([FromRoute] Guid roomId) { + var command = new RemoveRoom.Command() + { + RoomId = roomId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// /// Enable a room /// - /// Enable room details + /// Id of the room to be enabled /// A RoomDto of the enabled room - [HttpPut("enable")] + [HttpPut("enable/{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableRoom([FromBody] EnableRoomCommand command) + public async Task>> EnableRoom([FromRoute] Guid roomId) { + var command = new EnableRoom.Command() + { + RoomId = roomId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } /// - /// Update a room + /// Disable a room /// - /// Id of the room to be updated - /// Update room details - /// A RoomDto of the updated room - [HttpPut("{roomId:guid}")] + /// Id of the room to be disabled + /// A RoomDto of the disabled room + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPut("disable/{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) + public async Task>> DisableRoom([FromRoute] Guid roomId) { - Console.WriteLine(request.Description); - var command = new UpdateRoomCommand() + var command = new DisableRoom.Command() { RoomId = roomId, - Name = request.Name, - Description = request.Description, - Capacity = request.Capacity, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// - /// Get a room by id + /// Update a room /// - /// Id of the room to be retrieved - /// A RoomDto of the retrieved room - [HttpGet("{roomId:guid}")] + /// Id of the room to be updated + /// Update room details + /// A RoomDto of the updated room + [HttpPut("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid roomId) + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) { - var query = new GetRoomByIdQuery() + var command = new UpdateRoom.Command() { RoomId = roomId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, }; - var result = await Mediator.Send(query); + var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Get all rooms paginated - /// - /// The page index - /// The size number - /// Criteria - /// The order in which the rooms are sorted - /// A paginated list of rooms - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllPaginated(int? page, int? size, string? sortBy, string? sortOrder) - { - var query = new GetAllRoomsPaginatedQuery() - { - Page = page, - Size = size, - SortBy = sortBy, - SortOrder = sortOrder - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 1ad6fff1..048f1b4c 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -1,12 +1,9 @@ using Api.Controllers.Payload.Requests.Staffs; using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; using Application.Identity; -using Application.Staffs.Commands.RemoveStaffFromRoom; -using Application.Staffs.Queries.GetAllStaffsPaginated; -using Application.Staffs.Queries.GetStaffById; -using Application.Staffs.Queries.GetStaffByRoom; -using Application.Staffs.Commands.AddStaff; -using Application.Users.Queries.Physical; +using Application.Staffs.Commands; +using Application.Staffs.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -25,7 +22,7 @@ public class StaffsController : ApiControllerBase [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid staffId) { - var query = new GetStaffByIdQuery() + var query = new GetStaffById.Query() { StaffId = staffId }; @@ -44,7 +41,7 @@ public async Task>> GetById([FromRoute] Guid staff [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetByRoom([FromRoute] Guid roomId) { - var query = new GetStaffByRoomQuery() + var query = new GetStaffByRoom.Query() { RoomId = roomId }; @@ -63,8 +60,9 @@ public async Task>> GetByRoom([FromRoute] Guid roo public async Task>>> GetAllPaginated( [FromQuery] GetAllStaffsPaginatedQueryParameters queryParameters) { - var query = new GetAllStaffsPaginatedQuery() + var query = new GetAllStaffsPaginated.Query() { + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, @@ -74,13 +72,23 @@ public async Task>>> GetAllPaginated return Ok(Result>.Succeed(result)); } + /// + /// Add a staff + /// + /// Add staff details + /// A StaffDto of the added staff [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> AddStaff([FromBody] AddStaffCommand command) + public async Task>> Add([FromBody] AddStaffRequest request) { + var command = new AddStaff.Command() + { + RoomId = request.RoomId, + UserId = request.UserId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -89,19 +97,17 @@ public async Task>> AddStaff([FromBody] AddStaffCo /// Remove a staff from room /// /// Id of the staff to be removed from room - /// Remove details /// A StaffDto of the removed staff [HttpPut("{staffId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> RemoveStaffFromRoom([FromRoute] Guid staffId, - [FromBody] RemoveStaffFromRoomRequest request) + public async Task>> RemoveFromRoom( + [FromRoute] Guid staffId) { - var command = new RemoveStaffFromRoomCommand() + var command = new RemoveStaffFromRoom.Command() { StaffId = staffId, - RoomId = request.RoomId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index f7d93173..dc2b406e 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,17 +1,9 @@ using Api.Controllers.Payload.Requests.Users; using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; using Application.Identity; -using Application.Users.Commands.AddUser; -using Application.Users.Commands.DisableUser; -using Application.Users.Commands.EnableUser; -using Application.Users.Commands.UpdateUser; +using Application.Users.Commands; using Application.Users.Queries; -using Application.Users.Queries.GetAllUsersPaginated; -using Application.Users.Queries.GetUserById; -using Application.Users.Queries.GetUsersByName; using Infrastructure.Identity.Authorization; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Api.Controllers; @@ -27,9 +19,9 @@ public class UsersController : ApiControllerBase [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetUserById([FromRoute] Guid userId) + public async Task>> GetById([FromRoute] Guid userId) { - var query = new GetUserByIdQuery + var query = new GetUserById.Query { UserId = userId, }; @@ -40,7 +32,7 @@ public async Task>> GetUserById([FromRoute] Guid us /// /// Get all users paginated /// - /// Get all users query parameters + /// Get all users query parameters /// A paginated list of UserDto [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] @@ -48,7 +40,7 @@ public async Task>> GetUserById([FromRoute] Guid us public async Task>>> GetAllPaginated( [FromQuery] GetAllUsersPaginatedQueryParameters queryParameters) { - var query = new GetAllUsersPaginatedQuery() + var query = new GetAllUsersPaginated.Query() { DepartmentId = queryParameters.DepartmentId, SearchTerm = queryParameters.SearchTerm, @@ -61,37 +53,37 @@ public async Task>>> GetAllPaginated( return Ok(Result>.Succeed(result)); } + /// + /// Add a user + /// + /// Add user details + /// A UserDto of the added user [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddUser([FromBody] AddUserCommand command) + public async Task>> Add([FromBody] AddUserRequest request) { + var command = new AddUser.Command() + { + Username = request.Username, + Email = request.Email, + Password = request.Password, + FirstName = request.FirstName, + LastName = request.LastName, + Role = request.Role, + Position = request.Position, + DepartmentId = request.DepartmentId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - // [RequiresRole(IdentityData.Roles.Admin)] - // [HttpGet] - // [ProducesResponseType(StatusCodes.Status200OK)] - // [ProducesResponseType(StatusCodes.Status403Forbidden)] - // public async Task>>> GetUsersByName(string? searchTerm, int? page, int? size) - // { - // var query = new GetUsersByNameQuery - // { - // SearchTerm = searchTerm, - // Page = page, - // Size = size - // }; - // var result = await Mediator.Send(query); - // return Ok(Result>.Succeed(result)); - // } - + /// - /// Disable a user + /// Enable a user /// /// Id of the user to be enabled /// A UserDto of the enabled user @@ -100,9 +92,9 @@ public async Task>> AddUser([FromBody] AddUserComma [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> EnableUser([FromRoute] Guid userId) + public async Task>> Enable([FromRoute] Guid userId) { - var command = new EnableUserCommand() + var command = new EnableUser.Command() { UserId = userId }; @@ -110,14 +102,23 @@ public async Task>> EnableUser([FromRoute] Guid use return Ok(Result.Succeed(result)); } + /// + /// Disable a user + /// + /// Id of the user to be disabled + /// A UserDto of the disabled user [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost("disable")] + [HttpPut("disable/{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableUser([FromBody] DisableUserCommand command) + public async Task>> Disable([FromRoute] Guid userId) { + var command = new DisableUser.Command() + { + UserId = userId, + }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -135,11 +136,9 @@ public async Task>> DisableUser([FromBody] DisableU [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) { - var command = new UpdateUserCommand() + var command = new UpdateUser.Command() { UserId = userId, - Username = request.Username, - Email = request.Email, FirstName = request.FirstName, LastName = request.LastName, Role = request.Role, diff --git a/src/Api/Extensions/WebApplicationExtensions.cs b/src/Api/Extensions/WebApplicationExtensions.cs index e0a90371..63e834ee 100644 --- a/src/Api/Extensions/WebApplicationExtensions.cs +++ b/src/Api/Extensions/WebApplicationExtensions.cs @@ -1,10 +1,12 @@ using Api.Middlewares; +using Infrastructure.Persistence; +using Serilog; namespace Api.Extensions; public static class WebApplicationExtensions { - public static void UseInfrastructure(this WebApplication app) + public static void UseInfrastructure(this WebApplication app, IConfiguration configuration) { // Configure the HTTP request pipeline. @@ -15,10 +17,20 @@ public static void UseInfrastructure(this WebApplication app) { app.UseSwagger(); app.UseSwaggerUI(); + app.UseCors("AllowAllOrigins"); + + app.MigrateDatabase((context, _) => + { + ApplicationDbContextSeed.Seed(context, configuration, Log.Logger).Wait(); + }); } - else + + if (app.Environment.IsEnvironment("Testing")) { + app.MigrateDatabase((_, _) => + { + }); } app.UseAuthentication(); diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 42a37f8f..829f0873 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -21,13 +21,9 @@ var app = builder.Build(); - app.UseInfrastructure(); + app.UseInfrastructure(builder.Configuration); - app.MigrateDatabase((context, _) => - { - ApplicationDbContextSeed.Seed(context, builder.Configuration, Log.Logger).Wait(); - }) - .Run(); + app.Run(); } catch (Exception ex) { diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 34a4b5be..d8b6c481 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -1,6 +1,5 @@ using System.IdentityModel.Tokens.Jwt; using Application.Common.Interfaces; -using Application.Identity; namespace Api.Services; diff --git a/src/Api/appsettings.Testing.json b/src/Api/appsettings.Testing.json new file mode 100644 index 00000000..5dbdc362 --- /dev/null +++ b/src/Api/appsettings.Testing.json @@ -0,0 +1,20 @@ +{ + "JweSettings": { + "SigningKeyId": "4bd28be8eac5414fb01c5cbe343b50144bd2", + "EncryptionKeyId": "4bd28be8eac5414fb01c5cbe343b5014", + "TokenLifetime": "00:20:00", + "RefreshTokenLifetimeInDays": 3 + }, + "Seed": true, + "Serilog" : { + "MinimumLevel" : { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Authentication": "Debug", + "System": "Warning" + } + } + } +} diff --git a/src/Application/Common/Models/Dtos/DepartmentDto.cs b/src/Application/Common/Models/Dtos/DepartmentDto.cs index 286a4b08..68d00db4 100644 --- a/src/Application/Common/Models/Dtos/DepartmentDto.cs +++ b/src/Application/Common/Models/Dtos/DepartmentDto.cs @@ -1,10 +1,12 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; using Domain.Entities; -namespace Application.Users.Queries; +namespace Application.Common.Models.Dtos; public class DepartmentDto : IMapFrom { public Guid Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } = null!; + public RoomDto? Room { get; set; } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated/DocumentItemDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs similarity index 76% rename from src/Application/Documents/Queries/GetAllDocumentsPaginated/DocumentItemDto.cs rename to src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs index 86cc8533..7420ad90 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated/DocumentItemDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs @@ -1,12 +1,9 @@ using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; -using AutoMapper; using Domain.Entities.Physical; -namespace Application.Documents.Queries.GetAllDocumentsPaginated; +namespace Application.Common.Models.Dtos.Physical; -[Obsolete] public class DocumentItemDto : IMapFrom { public Guid Id { get; set; } diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyFolderDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs similarity index 89% rename from src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyFolderDto.cs rename to src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs index 0398e867..10e77646 100644 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyFolderDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs @@ -2,7 +2,7 @@ using AutoMapper; using Domain.Entities.Physical; -namespace Application.Rooms.Queries.GetEmptyContainersPaginated; +namespace Application.Common.Models.Dtos.Physical; public class EmptyFolderDto : IMapFrom { diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyLockerDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs similarity index 86% rename from src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyLockerDto.cs rename to src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs index f9f80d7b..6d7537c7 100644 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/EmptyLockerDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs @@ -1,9 +1,8 @@ using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; -namespace Application.Rooms.Queries.GetEmptyContainersPaginated; +namespace Application.Common.Models.Dtos.Physical; public class EmptyLockerDto : IMapFrom { diff --git a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs index 5426161f..a2ed7b95 100644 --- a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs @@ -1,5 +1,5 @@ using Application.Common.Mappings; -using Application.Users.Queries.Physical; +using Application.Users.Queries; using AutoMapper; using Domain.Entities.Physical; @@ -8,9 +8,10 @@ namespace Application.Common.Models.Dtos.Physical; public class RoomDto : IMapFrom { public Guid Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public StaffDto Staff { get; set; } + public string Name { get; set; } = null!; + public string? Description { get; set; } + public StaffDto? Staff { get; set; } + public DepartmentDto? Department { get; set; } public int Capacity { get; set; } public int NumberOfLockers { get; set; } public bool IsAvailable { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs index b4ff5a80..84563c1b 100644 --- a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs @@ -1,11 +1,11 @@ using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; using Domain.Entities.Physical; -namespace Application.Users.Queries.Physical; +namespace Application.Common.Models.Dtos.Physical; public class StaffDto : IMapFrom { - public UserDto User { get; set; } - public RoomDto Room { get; set; } + public UserDto User { get; set; } = null!; + public RoomDto? Room { get; set; } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index ccb4ba64..64cf73dc 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -1,4 +1,5 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos; using AutoMapper; using Domain.Entities; diff --git a/src/Application/Departments/Commands/AddDepartment.cs b/src/Application/Departments/Commands/AddDepartment.cs new file mode 100644 index 00000000..525d83c4 --- /dev/null +++ b/src/Application/Departments/Commands/AddDepartment.cs @@ -0,0 +1,50 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Departments.Commands; + +public class AddDepartment +{ + public record Command : IRequest + { + public string Name { get; init; } = null!; + } + + public class AddDepartmentCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public AddDepartmentCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var department = await _context.Departments.FirstOrDefaultAsync(x + => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); + + if (department is not null) + { + throw new ConflictException("Department name already exists."); + } + + var entity = new Department + { + Name = request.Name + }; + + var result = await _context.Departments.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs b/src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs deleted file mode 100644 index 00f42e5c..00000000 --- a/src/Application/Departments/Commands/AddDepartment/AddDepartmentCommand.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Departments.Commands.AddDepartment; - -public record AddDepartmentCommand : IRequest -{ - public string Name { get; init; } = null!; -} - -public class AddDepartmentCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public AddDepartmentCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(AddDepartmentCommand request, CancellationToken cancellationToken) - { - var department = await _context.Departments.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); - - if (department is not null) - { - throw new ConflictException("Department name already exists."); - } - var entity = new Department - { - Name = request.Name - }; - - var result = await _context.Departments.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Departments/Commands/DeleteDepartment.cs b/src/Application/Departments/Commands/DeleteDepartment.cs new file mode 100644 index 00000000..b744630b --- /dev/null +++ b/src/Application/Departments/Commands/DeleteDepartment.cs @@ -0,0 +1,41 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Users.Queries; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Departments.Commands; + +public class DeleteDepartment +{ + public record Command : IRequest + { + public Guid DepartmentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var department = await _context.Departments.FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); + + if (department is null) + { + throw new KeyNotFoundException("Department does not exist."); + } + + var result = _context.Departments.Remove(department); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Departments/Commands/DeleteDepartment/DeleteDepartmentCommand.cs b/src/Application/Departments/Commands/DeleteDepartment/DeleteDepartmentCommand.cs deleted file mode 100644 index 93ea7708..00000000 --- a/src/Application/Departments/Commands/DeleteDepartment/DeleteDepartmentCommand.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Users.Queries; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Departments.Commands.DeleteDepartment; - -public record DeleteDepartmentCommand : IRequest -{ - public Guid DepartmentId { get; init; } -} - -public class DeleteDepartmentCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public DeleteDepartmentCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(DeleteDepartmentCommand request, CancellationToken cancellationToken) - { - var department = await _context.Departments.FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); - - if (department is null) - { - throw new KeyNotFoundException("Department does not exist"); - } - - var result = _context.Departments.Remove(department); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Departments/Commands/UpdateDepartment.cs b/src/Application/Departments/Commands/UpdateDepartment.cs new file mode 100644 index 00000000..a080e22d --- /dev/null +++ b/src/Application/Departments/Commands/UpdateDepartment.cs @@ -0,0 +1,14 @@ +using Application.Common.Models.Dtos; +using Application.Users.Queries; +using MediatR; + +namespace Application.Departments.Commands; + +public class UpdateDepartment +{ + public record Command : IRequest + { + public Guid DepartmentId { get; set; } + public string Name { get; init; } = null!; + } +} \ No newline at end of file diff --git a/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs b/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs deleted file mode 100644 index 941879af..00000000 --- a/src/Application/Departments/Commands/UpdateDepartment/UpdateDepartmentCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Departments.Commands.UpdateDepartment; - -public record UpdateDepartmentCommand : IRequest -{ - public Guid DepartmentId { get; set; } - public string Name { get; init; } = null!; -} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetAllDepartments.cs b/src/Application/Departments/Queries/GetAllDepartments.cs new file mode 100644 index 00000000..a4e5d711 --- /dev/null +++ b/src/Application/Departments/Queries/GetAllDepartments.cs @@ -0,0 +1,32 @@ +using System.Collections.ObjectModel; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Departments.Queries; + +public class GetAllDepartments +{ + public record Query : IRequest>; + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var departments = await _context.Departments.ToListAsync(cancellationToken); + var result = new ReadOnlyCollection(_mapper.Map>(departments)); + return result; + } + } +} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetAllDepartments/GetAllDepartmentsQuery.cs b/src/Application/Departments/Queries/GetAllDepartments/GetAllDepartmentsQuery.cs deleted file mode 100644 index a0af15e1..00000000 --- a/src/Application/Departments/Queries/GetAllDepartments/GetAllDepartmentsQuery.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.ObjectModel; -using Application.Common.Interfaces; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Departments.Queries.GetAllDepartments; - -public record GetAllDepartmentsQuery : IRequest>; - -public class GetAllDepartmentsQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public GetAllDepartmentsQueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(GetAllDepartmentsQuery request, CancellationToken cancellationToken) - { - var departments = await _context.Departments.ToListAsync(cancellationToken); - var result = new ReadOnlyCollection(_mapper.Map>(departments)); - return result; - } -} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetDepartmentById.cs b/src/Application/Departments/Queries/GetDepartmentById.cs new file mode 100644 index 00000000..e4120c64 --- /dev/null +++ b/src/Application/Departments/Queries/GetDepartmentById.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos; +using MediatR; + +namespace Application.Departments.Queries; + +public class GetDepartmentById +{ + public record Query : IRequest + { + public Guid DepartmentId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs b/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs deleted file mode 100644 index fe0ec11a..00000000 --- a/src/Application/Departments/Queries/GetDepartmentById/GetDepartmentByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Departments.Queries.GetDepartmentById; - -public record GetDepartmentByIdQuery : IRequest -{ - public Guid DepartmentId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs new file mode 100644 index 00000000..7b22f634 --- /dev/null +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Documents.Commands; + +public class DeleteDocument +{ + public record Command : IRequest + { + public Guid DocumentId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs b/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs deleted file mode 100644 index eefecf87..00000000 --- a/src/Application/Documents/Commands/DeleteDocument/DeleteDocumentCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Documents.Commands.DeleteDocument; - -public record DeleteDocumentCommand : IRequest -{ - public Guid DocumentId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs new file mode 100644 index 00000000..2f5e8c02 --- /dev/null +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -0,0 +1,76 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Commands; + +public class ImportDocument +{ + public record Command : IRequest + { + public string Title { get; init; } = null!; + public string? Description { get; init; } + public string DocumentType { get; init; } = null!; + public Guid ImporterId { get; init; } + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var importer = await _context.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); + if (importer is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + var document = _context.Documents.FirstOrDefault(x => + x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) + && x.Importer != null + && x.Importer.Id == request.ImporterId); + if (document is not null) + { + throw new ConflictException($"Document title already exists for user {importer.LastName}."); + } + + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + var entity = new Document() + { + Title = request.Title.Trim(), + Description = request.Description?.Trim(), + DocumentType = request.DocumentType.Trim(), + Importer = importer, + Department = importer.Department, + Folder = folder + }; + + var result = await _context.Documents.AddAsync(entity, cancellationToken); + folder.NumberOfDocuments += 1; + _context.Folders.Update(folder); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs b/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs deleted file mode 100644 index 7989bf5a..00000000 --- a/src/Application/Documents/Commands/ImportDocument/ImportDocumentCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Commands.ImportDocument; - -public record ImportDocumentCommand : IRequest -{ - public string Title { get; init; } = null!; - public string? Description { get; init; } - public string DocumentType { get; init; } = null!; - public Guid ImporterId { get; init; } - public Guid FolderId { get; init; } -} - -public class ImportDocumentCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public ImportDocumentCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(ImportDocumentCommand request, CancellationToken cancellationToken) - { - var importer = await _context.Users - .Include(x => x.Department) - .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); - if (importer is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - var document = _context.Documents.FirstOrDefault(x => - x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) - && x.Importer != null - && x.Importer.Id == request.ImporterId); - if (document is not null) - { - throw new ConflictException($"Document title already exists for user {importer.LastName}."); - } - - var folder = await _context.Folders - .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - var entity = new Document() - { - Title = request.Title.Trim(), - Description = request.Description?.Trim(), - DocumentType = request.DocumentType.Trim(), - Importer = importer, - Department = importer.Department, - Folder = folder - }; - - var result = await _context.Documents.AddAsync(entity, cancellationToken); - folder.NumberOfDocuments += 1; - _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs new file mode 100644 index 00000000..b0cac569 --- /dev/null +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -0,0 +1,15 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Documents.Commands; + +public class UpdateDocument +{ + public record Command : IRequest + { + public Guid DocumentId { get; init; } + public string Title { get; init; } = null!; + public string? Description { get; init; } + public string DocumentType { get; init; } = null!; + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs b/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs deleted file mode 100644 index d7b06736..00000000 --- a/src/Application/Documents/Commands/UpdateDocument/UpdateDocumentCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Documents.Commands.UpdateDocument; - -public record UpdateDocumentCommand : IRequest -{ - public Guid DocumentId { get; init; } - public string Title { get; init; } = null!; - public string? Description { get; init; } - public string DocumentType { get; init; } = null!; -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentTypes.cs b/src/Application/Documents/Queries/GetAllDocumentTypes.cs new file mode 100644 index 00000000..41e53d1b --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentTypes.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using Application.Common.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentTypes +{ + public record Query : IRequest>; + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + return new ReadOnlyCollection(await _context.Documents.Select(x => x.DocumentType).Distinct() + .ToListAsync(cancellationToken)); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs new file mode 100644 index 00000000..c6c34a92 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -0,0 +1,138 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentsPaginated +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .Must((query, roomId) => + { + if (roomId is null) + { + return query.LockerId is null && query.FolderId is null; + } + + if (query.LockerId is null) + { + return query.FolderId is null; + } + + return true; + }).WithMessage("Container orientation is not consistent"); + } + } + + public record Query : IRequest> + { + public Guid? RoomId { get; init; } + public Guid? LockerId { get; init; } + public Guid? FolderId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents.AsQueryable(); + var roomExists = request.RoomId is not null; + var lockerExists = request.LockerId is not null; + var folderExists = request.FolderId is not null; + + documents = documents.Include(x => x.Department); + + if (folderExists) + { + var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(y => y.Room) + .FirstOrDefaultAsync(x => x.Id == request.FolderId + && x.IsAvailable, cancellationToken); + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + if (folder.Locker.Id != request.LockerId + || folder.Locker.Room.Id != request.RoomId) + { + throw new ConflictException("Either locker or room does not match folder."); + } + + documents = documents + .Where(x => x.Folder!.Id == request.FolderId); + } + else if (lockerExists) + { + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.LockerId + && x.IsAvailable, cancellationToken); + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.Room.Id != request.RoomId) + { + throw new ConflictException("Room does not match locker."); + } + + documents = documents.Where(x => x.Folder!.Locker.Id == request.LockerId); + } + else if (roomExists) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId + && x.IsAvailable, cancellationToken); + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + documents = documents.Where(x => x.Folder!.Locker.Room.Id == request.RoomId); + } + + var sortBy = request.SortBy ?? nameof(DocumentDto.Id); + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page ?? 1; + var sizeNumber = request.Size ?? 5; + var result = await documents + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber, sizeNumber); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQuery.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQuery.cs deleted file mode 100644 index ff2de9ad..00000000 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQuery.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Mappings; -using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries.GetAllDocumentsPaginated; - -public record GetAllDocumentsPaginatedQuery : IRequest> -{ - public Guid? RoomId { get; init; } - public Guid? LockerId { get; init; } - public Guid? FolderId { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } -} - -public class GetAllDocumentsPaginatedQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public GetAllDocumentsPaginatedQueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(GetAllDocumentsPaginatedQuery request, - CancellationToken cancellationToken) - { - var documents = _context.Documents.AsQueryable(); - var roomExists = request.RoomId is not null; - var lockerExists = request.LockerId is not null; - var folderExists = request.FolderId is not null; - - documents = documents.Include(x => x.Department); - - if (folderExists) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(y => y.Room) - .FirstOrDefaultAsync(x => x.Id == request.FolderId - && x.IsAvailable, cancellationToken); - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (folder.Locker.Id != request.LockerId - || folder.Locker.Room.Id != request.RoomId) - { - throw new ConflictException("Either locker or room does not match folder."); - } - - documents = documents - .Where(x => x.Folder!.Id == request.FolderId); - } - else if (lockerExists) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.Id == request.LockerId - && x.IsAvailable, cancellationToken); - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.Room.Id != request.RoomId) - { - throw new ConflictException("Room does not match locker."); - } - - documents = documents.Where(x => x.Folder!.Locker.Id == request.LockerId); - } - else if (roomExists) - { - var room = await _context.Rooms - .FirstOrDefaultAsync(x => x.Id == request.RoomId - && x.IsAvailable, cancellationToken); - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - documents = documents.Where(x => x.Folder!.Locker.Room.Id == request.RoomId); - } - - var sortBy = request.SortBy ?? nameof(DocumentDto.Id); - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page ?? 1; - var sizeNumber = request.Size ?? 5; - var result = await documents - .ProjectTo(_mapper.ConfigurationProvider) - .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber, sizeNumber); - - return result; - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQueryValidator.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQueryValidator.cs deleted file mode 100644 index d95c7e3d..00000000 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated/GetAllDocumentsPaginatedQueryValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentValidation; - -namespace Application.Documents.Queries.GetAllDocumentsPaginated; - -public class GetAllDocumentsPaginatedQueryValidator : AbstractValidator -{ - public GetAllDocumentsPaginatedQueryValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .Must((query, roomId) => - { - if (roomId is null) - { - return query.LockerId is null && query.FolderId is null; - } - - if (query.LockerId is null) - { - return query.FolderId is null; - } - - return true; - }).WithMessage("Container orientation is not consistent"); - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs new file mode 100644 index 00000000..bf7c29d7 --- /dev/null +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -0,0 +1,44 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetDocumentById +{ + public record Query : IRequest + { + public Guid DocumentId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .Include(x => x.Importer) + .Include(x => x.Folder) + .ThenInclude(y => y.Locker) + .ThenInclude(z => z.Room) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + return _mapper.Map(document); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentById/GetDocumentByIdQuery.cs b/src/Application/Documents/Queries/GetDocumentById/GetDocumentByIdQuery.cs deleted file mode 100644 index c1f62b70..00000000 --- a/src/Application/Documents/Queries/GetDocumentById/GetDocumentByIdQuery.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries.GetDocumentById; - -public record GetDocumentByIdQuery : IRequest -{ - public Guid Id { get; init; } -} - -public class GetDocumentByIdQueryHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public GetDocumentByIdQueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public async Task Handle(GetDocumentByIdQuery request, CancellationToken cancellationToken) - { - var document = await _context.Documents - .Include(x => x.Department) - .Include(x => x.Importer) - .Include(x => x.Folder) - .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room) - .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken); - - if (document is null) - { - throw new KeyNotFoundException("Document does not exist."); - } - - return _mapper.Map(document); - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentTypes/GetAllDocumentTypesQuery.cs b/src/Application/Documents/Queries/GetDocumentTypes/GetAllDocumentTypesQuery.cs deleted file mode 100644 index 841aa23e..00000000 --- a/src/Application/Documents/Queries/GetDocumentTypes/GetAllDocumentTypesQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.ObjectModel; -using Application.Common.Interfaces; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries.GetDocumentTypes; - -public record GetAllDocumentTypesQuery : IRequest>; - -public class GetAllDocumentTypesQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - - public GetAllDocumentTypesQueryHandler(IApplicationDbContext context) - { - _context = context; - } - public async Task> Handle(GetAllDocumentTypesQuery request, CancellationToken cancellationToken) - { - return new ReadOnlyCollection(await _context.Documents.Select(x => x.DocumentType).Distinct() - .ToListAsync(cancellationToken)); - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentTypes/GetDocumentTypesQuery.cs b/src/Application/Documents/Queries/GetDocumentTypes/GetDocumentTypesQuery.cs deleted file mode 100644 index b6f65030..00000000 --- a/src/Application/Documents/Queries/GetDocumentTypes/GetDocumentTypesQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.ObjectModel; -using Application.Common.Interfaces; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries.GetDocumentTypes; - -public record GetDocumentTypesQuery : IRequest>; - -public class GetDocumentTypesQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - - public GetDocumentTypesQueryHandler(IApplicationDbContext context) - { - _context = context; - } - public async Task> Handle(GetDocumentTypesQuery request, CancellationToken cancellationToken) - { - return new ReadOnlyCollection(await _context.Documents.Select(x => x.DocumentType) - .ToListAsync(cancellationToken)); - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs new file mode 100644 index 00000000..4b7bd9b9 --- /dev/null +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -0,0 +1,95 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Exceptions; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Folders.Commands; + +public class AddFolder +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(f => f.Name) + .NotEmpty().WithMessage("Name is required.") + .MaximumLength(64).WithMessage("Name cannot exceed 64 characters."); + + RuleFor(f => f.Description) + .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); + + RuleFor(f => f.Capacity) + .NotEmpty().WithMessage("Folder capacity is required.") + .GreaterThanOrEqualTo(1).WithMessage("Folder's capacity cannot be less than 1."); + + RuleFor(f => f.LockerId) + .NotEmpty().WithMessage("LockerId is required."); + } + } + + public record Command : IRequest + { + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + public Guid LockerId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); + + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.NumberOfFolders >= locker.Capacity) + { + throw new LimitExceededException("This locker cannot accept more folders."); + } + + var folder = await _context.Folders.FirstOrDefaultAsync(x => + x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) + && x.Locker.Id.Equals(request.LockerId), cancellationToken); + + if (folder is not null) + { + throw new ConflictException("Folder name already exists."); + } + + var entity = new Folder + { + Name = request.Name.Trim(), + Description = request.Description, + NumberOfDocuments = 0, + Capacity = request.Capacity, + Locker = locker, + IsAvailable = true + }; + var result = await _context.Folders.AddAsync(entity, cancellationToken); + locker.NumberOfFolders += 1; + _context.Lockers.Update(locker); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs b/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs deleted file mode 100644 index bf974013..00000000 --- a/src/Application/Folders/Commands/AddFolder/AddFolderCommand.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using Domain.Exceptions; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands.AddFolder; - -public record AddFolderCommand : IRequest -{ - public string Name { get; init; } = null!; - public string? Description { get; init; } - public int Capacity { get; init; } - public Guid LockerId { get; init; } -} - -public class AddFolderCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public AddFolderCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(AddFolderCommand request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers.FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); - - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.NumberOfFolders >= locker.Capacity) - { - throw new LimitExceededException("This locker cannot accept more folders."); - } - - var folder = await _context.Folders.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Locker.Id.Equals(request.LockerId), cancellationToken); - - if (folder is not null) - { - throw new ConflictException("Folder's name already exists."); - } - - var entity = new Folder - { - Name = request.Name.Trim(), - Description = request.Description, - NumberOfDocuments = 0, - Capacity = request.Capacity, - Locker = locker, - IsAvailable = true - }; - var result = await _context.Folders.AddAsync(entity, cancellationToken); - locker.NumberOfFolders += 1; - _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} diff --git a/src/Application/Folders/Commands/AddFolder/AddFolderCommandValidator.cs b/src/Application/Folders/Commands/AddFolder/AddFolderCommandValidator.cs deleted file mode 100644 index f7717f54..00000000 --- a/src/Application/Folders/Commands/AddFolder/AddFolderCommandValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentValidation; - -namespace Application.Folders.Commands.AddFolder; - -public class AddFolderCommandValidator : AbstractValidator -{ - public AddFolderCommandValidator() - { - - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(f => f.Name) - .NotEmpty().WithMessage("Name is required.") - .MaximumLength(64).WithMessage("Name cannot exceed 64 characters."); - - RuleFor(f => f.Description) - .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); - - RuleFor(f => f.Capacity) - .NotEmpty().WithMessage("Folder capacity is required.") - .GreaterThanOrEqualTo(1).WithMessage("Folder's capacity cannot be less than 1."); - - RuleFor(f => f.LockerId) - .NotEmpty().WithMessage("LockerId is required."); - } - -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/DisableFolder.cs b/src/Application/Folders/Commands/DisableFolder.cs new file mode 100644 index 00000000..020bc1fe --- /dev/null +++ b/src/Application/Folders/Commands/DisableFolder.cs @@ -0,0 +1,66 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Folders.Commands; + +public class DisableFolder +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(f => f.FolderId) + .NotEmpty().WithMessage("FolderId is required."); + } + } + + public record Command : IRequest + { + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var folder = await _context.Folders + .FirstOrDefaultAsync(f => f.Id.Equals(request.FolderId), cancellationToken); + + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + if (!folder.IsAvailable) + { + throw new ConflictException("Folder has already been disabled."); + } + + if (folder.NumberOfDocuments > 0) + { + throw new InvalidOperationException("Folder cannot be disabled because it contains documents."); + } + + folder.IsAvailable = false; + _context.Folders.Update(folder); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(folder); + } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs b/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs deleted file mode 100644 index adf845df..00000000 --- a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommand.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands.DisableFolder; - -public record DisableFolderCommand : IRequest -{ - public Guid FolderId { get; init; } -} - -public class DisableFolderCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public DisableFolderCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(DisableFolderCommand request, CancellationToken cancellationToken) - { - var folder = await _context.Folders - .FirstOrDefaultAsync(f => f.Id.Equals(request.FolderId), cancellationToken); - - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (!folder.IsAvailable) - { - throw new ConflictException("Folder has already been disabled."); - } - - if (folder.NumberOfDocuments > 0) - { - throw new InvalidOperationException("Folder cannot be disabled because it contains documents."); - } - - folder.IsAvailable = false; - _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(folder); - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommandValidator.cs b/src/Application/Folders/Commands/DisableFolder/DisableFolderCommandValidator.cs deleted file mode 100644 index 04621166..00000000 --- a/src/Application/Folders/Commands/DisableFolder/DisableFolderCommandValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentValidation; - -namespace Application.Folders.Commands.DisableFolder; - -public class DisableFolderCommandValidator : AbstractValidator -{ - public DisableFolderCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(f => f.FolderId) - .NotEmpty().WithMessage("FolderId is required."); - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/EnableFolder.cs b/src/Application/Folders/Commands/EnableFolder.cs new file mode 100644 index 00000000..33e3cb23 --- /dev/null +++ b/src/Application/Folders/Commands/EnableFolder.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands; + +public class EnableFolder +{ + public record Command : IRequest + { + public Guid FolderId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs b/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs deleted file mode 100644 index eb7f49f9..00000000 --- a/src/Application/Folders/Commands/EnableFolder/EnableFolderCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Folders.Commands.EnableFolder; - -public record EnableFolderCommand : IRequest -{ - public Guid FolderId { get; init; } -} diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs new file mode 100644 index 00000000..528184ce --- /dev/null +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands; + +public class RemoveFolder +{ + public record Command : IRequest + { + public Guid FolderId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs b/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs deleted file mode 100644 index 91db8b5e..00000000 --- a/src/Application/Folders/Commands/RemoveFolder/RemoveFolderCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Folders.Commands.RemoveFolder; - -public record RemoveFolderCommand : IRequest -{ - public Guid FolderId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs new file mode 100644 index 00000000..a467f7e3 --- /dev/null +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -0,0 +1,15 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Commands; + +public class UpdateFolder +{ + public record Command : IRequest + { + public Guid FolderId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs b/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs deleted file mode 100644 index 30ebea09..00000000 --- a/src/Application/Folders/Commands/UpdateFolder/UpdateFolderCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Folders.Commands.UpdateFolder; - -public record UpdateFolderCommand : IRequest -{ - public Guid FolderId { get; init; } - public string Name { get; init; } = null!; - public string? Description { get; init; } - public int Capacity { get; init; } -} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs new file mode 100644 index 00000000..280a4c5c --- /dev/null +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -0,0 +1,18 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Queries; + +public class GetAllFoldersPaginated +{ + public record Query : IRequest> + { + public Guid? RoomId { get; init; } + public Guid? LockerId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs deleted file mode 100644 index ecd9fbb2..00000000 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated/GetAllFoldersPaginatedQuery.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Folders.Queries.GetAllFoldersPaginated; - -public record GetAllFoldersPaginatedQuery : IRequest> -{ - public Guid? RoomId { get; init; } - public Guid? LockerId { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } -} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs new file mode 100644 index 00000000..fd0d0b57 --- /dev/null +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Folders.Queries; + +public class GetFolderById +{ + public record Query : IRequest + { + public Guid FolderId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs b/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs deleted file mode 100644 index bf422997..00000000 --- a/src/Application/Folders/Queries/GetFolderById/GetFolderByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Folders.Queries.GetFolderById; - -public record GetFolderByIdQuery : IRequest -{ - public Guid FolderId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Identity/IdentityData.cs b/src/Application/Identity/IdentityData.cs index a9742b72..6e69dbb7 100644 --- a/src/Application/Identity/IdentityData.cs +++ b/src/Application/Identity/IdentityData.cs @@ -2,15 +2,10 @@ namespace Application.Identity; public static class IdentityData { - public static class Claims - { - public const string Role = "role"; - public const string Department = "department"; - } - public static class Roles { public const string Admin = "Admin"; public const string Staff = "Staff"; + public const string Employee = "Employee"; } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs new file mode 100644 index 00000000..78cbb371 --- /dev/null +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -0,0 +1,96 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Exceptions; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Commands; + +public class AddLocker +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Capacity) + .GreaterThan(0).WithMessage("Locker's capacity cannot be less than 1"); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Locker's name is required.") + .MaximumLength(64).WithMessage("Locker's name cannot exceed 64 characters."); + + RuleFor(x => x.Description) + .MaximumLength(256).WithMessage("Locker's description cannot exceed 256 characters."); + + RuleFor(x => x.RoomId) + .NotEmpty().WithMessage("RoomId is required."); + } + } + + public record Command : IRequest + { + public string Name { get; init; } = null!; + public string? Description { get; init; } + public Guid RoomId { get; init; } + public int Capacity { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + if (room.NumberOfLockers >= room.Capacity) + { + throw new LimitExceededException( + "This room cannot accept more lockers." + ); + } + + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Room.Id.Equals(request.RoomId), + cancellationToken); + if (locker is not null) + { + throw new ConflictException("Locker name already exists."); + } + + var entity = new Locker + { + Name = request.Name.Trim(), + Description = request.Description?.Trim(), + NumberOfFolders = 0, + Capacity = request.Capacity, + Room = room, + IsAvailable = true + }; + + var result = await _context.Lockers.AddAsync(entity, cancellationToken); + room.NumberOfLockers += 1; + _context.Rooms.Update(room); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs b/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs deleted file mode 100644 index 7a47bd87..00000000 --- a/src/Application/Lockers/Commands/AddLocker/AddLockerCommand.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using Domain.Exceptions; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands.AddLocker; - -public record AddLockerCommand : IRequest -{ - public string Name { get; init; } = null!; - public string? Description { get; init; } - public Guid RoomId { get; init; } - public int Capacity { get; init; } -} - -public class AddLockerCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public AddLockerCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(AddLockerCommand request, CancellationToken cancellationToken) - { - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (room.NumberOfLockers >= room.Capacity) - { - throw new LimitExceededException( - "This room cannot accept more lockers." - ); - } - - var locker = await _context.Lockers.FirstOrDefaultAsync(x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Room.Id.Equals(request.RoomId) ,cancellationToken); - if (locker is not null) - { - throw new ConflictException("Locker name already exists."); - } - - var entity = new Locker - { - Name = request.Name.Trim(), - Description = request.Description?.Trim(), - NumberOfFolders = 0, - Capacity = request.Capacity, - Room = room, - IsAvailable = true - }; - - var result = await _context.Lockers.AddAsync(entity, cancellationToken); - room.NumberOfLockers += 1; - _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker/AddLockerCommandValidator.cs b/src/Application/Lockers/Commands/AddLocker/AddLockerCommandValidator.cs deleted file mode 100644 index 4a919d32..00000000 --- a/src/Application/Lockers/Commands/AddLocker/AddLockerCommandValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Application.Common.Interfaces; -using FluentValidation; - -namespace Application.Lockers.Commands.AddLocker; - -public class AddLockerCommandValidator : AbstractValidator -{ - - public AddLockerCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.Capacity) - .GreaterThan(0).WithMessage("Locker's capacity cannot be less than 1"); - - RuleFor(x => x.Name) - .NotEmpty().WithMessage("Locker's name is required.") - .MaximumLength(64).WithMessage("Locker's name cannot exceed 64 characters."); - - RuleFor(x => x.Description) - .MaximumLength(256).WithMessage("Locker's description cannot exceed 256 characters."); - - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } -} diff --git a/src/Application/Lockers/Commands/DisableLocker.cs b/src/Application/Lockers/Commands/DisableLocker.cs new file mode 100644 index 00000000..eaa4f40b --- /dev/null +++ b/src/Application/Lockers/Commands/DisableLocker.cs @@ -0,0 +1,78 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Commands; + +public class DisableLocker +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.LockerId) + .NotEmpty().WithMessage("LockerId is required."); + } + } + + public record Command : IRequest + { + public Guid LockerId { get; init; } + } + + public class RemoveLockerCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public RemoveLockerCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers + .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); + + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (!locker.IsAvailable) + { + throw new ConflictException("Locker has already been disabled."); + } + + var canNotDisable = await _context.Documents + .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) + > 0; + + if (canNotDisable) + { + throw new InvalidOperationException("Locker cannot be disabled because it contains documents."); + } + + var folders = _context.Folders.Where(x => x.Locker.Room.Id.Equals(locker.Id)); + + foreach (var folder in folders) + { + folder.IsAvailable = false; + } + _context.Folders.UpdateRange(folders); + + locker.IsAvailable = false; + var result = _context.Lockers.Update(locker); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommand.cs b/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommand.cs deleted file mode 100644 index e3785e25..00000000 --- a/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommand.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands.DisableLocker; - -public record DisableLockerCommand : IRequest -{ - public Guid LockerId { get; init; } -} - -public class RemoveLockerCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public RemoveLockerCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(DisableLockerCommand request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (!locker.IsAvailable) - { - throw new ConflictException("Locker has already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Locker cannot be disabled because it contains documents."); - } - - var folders = _context.Folders.Where(x => x.Locker.Room.Id.Equals(locker.Id)); - - foreach (var folder in folders) - { - folder.IsAvailable = false; - } - _context.Folders.UpdateRange(folders); - - locker.IsAvailable = false; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommandValidator.cs b/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommandValidator.cs deleted file mode 100644 index e6192be6..00000000 --- a/src/Application/Lockers/Commands/DisableLocker/DisableLockerCommandValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation; - -namespace Application.Lockers.Commands.DisableLocker; - -public class DisableLockerCommandValidator : AbstractValidator -{ - public DisableLockerCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("Locker Id is required."); - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/EnableLocker.cs b/src/Application/Lockers/Commands/EnableLocker.cs new file mode 100644 index 00000000..32dee1aa --- /dev/null +++ b/src/Application/Lockers/Commands/EnableLocker.cs @@ -0,0 +1,60 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Commands; + +public class EnableLocker +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.LockerId) + .NotEmpty().WithMessage("LockerId is required."); + } + } + + public record Command : IRequest + { + public Guid LockerId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers + .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.IsAvailable) + { + throw new ConflictException("Locker has already been enabled."); + } + + locker.IsAvailable = true; + var result = _context.Lockers.Update(locker); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommand.cs b/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommand.cs deleted file mode 100644 index 4c1cb485..00000000 --- a/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Exceptions; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands.EnableLocker; - -public record EnableLockerCommand : IRequest -{ - public Guid LockerId { get; init; } -} - -public class EnableLockerCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public EnableLockerCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(EnableLockerCommand request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.IsAvailable) - { - throw new ConflictException("Locker has already been enabled."); - } - - locker.IsAvailable = true; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} diff --git a/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommandValidator.cs b/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommandValidator.cs deleted file mode 100644 index 2fcbbec9..00000000 --- a/src/Application/Lockers/Commands/EnableLocker/EnableLockerCommandValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation; - -namespace Application.Lockers.Commands.EnableLocker; - -public class EnableLockerCommandValidator : AbstractValidator -{ - public EnableLockerCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("Locker Id is required."); - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs new file mode 100644 index 00000000..11c6472b --- /dev/null +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Commands; + +public class RemoveLocker +{ + public record Command : IRequest + { + public Guid LockerId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs b/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs deleted file mode 100644 index 412df4b7..00000000 --- a/src/Application/Lockers/Commands/RemoveLocker/RemoveLockerCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Lockers.Commands.RemoveLocker; - -public record RemoveLockerCommand : IRequest -{ - public Guid LockerId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs new file mode 100644 index 00000000..580c1e6f --- /dev/null +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -0,0 +1,15 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Commands; + +public class UpdateLocker +{ + public record Command : IRequest + { + public Guid LockerId { get; init; } + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs b/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs deleted file mode 100644 index 540ceeef..00000000 --- a/src/Application/Lockers/Commands/UpdateLocker/UpdateLockerCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Lockers.Commands.UpdateLocker; - -public record UpdateLockerCommand : IRequest -{ - public Guid LockerId { get; set; } - public string Name { get; set; } = null!; - public string? Description { get; set; } - public int Capacity { get; init; } -} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs new file mode 100644 index 00000000..f7935335 --- /dev/null +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -0,0 +1,17 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Queries; + +public class GetAllLockersPaginated +{ + public record Query : IRequest> + { + public Guid? RoomId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs deleted file mode 100644 index 4e0e8eb7..00000000 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated/GetAllLockersPaginatedQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Lockers.Queries.GetAllLockersPaginated; - -public record GetAllLockersPaginatedQuery : IRequest> -{ - public Guid? RoomId { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } -} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs new file mode 100644 index 00000000..65c31416 --- /dev/null +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Lockers.Queries; + +public class GetLockerById +{ + public record Query : IRequest + { + public Guid LockerId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs b/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs deleted file mode 100644 index 1a592104..00000000 --- a/src/Application/Lockers/Queries/GetLockerById/GetLockerByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Lockers.Queries.GetLockerById; - -public record GetLockerByIdQuery : IRequest -{ - public Guid LockerId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs new file mode 100644 index 00000000..a317d7cf --- /dev/null +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -0,0 +1,91 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Commands; + +public class AddRoom +{ + public class Validator : AbstractValidator + { + private readonly IApplicationDbContext _context; + public Validator(IApplicationDbContext context) + { + _context = context; + + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Capacity) + .GreaterThanOrEqualTo(1).WithMessage("Room's capacity cannot be less than 1"); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .MaximumLength(64).WithMessage("Name cannot exceed 64 characters.") + .Must(BeUnique).WithMessage("Room name already exists."); + + RuleFor(x => x.Description) + .NotEmpty().WithMessage("Description is required.") + .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); + } + + private bool BeUnique(string name) + { + return _context.Rooms.FirstOrDefault(x => x.Name.Equals(name)) is null; + } + } + + public record Command : IRequest + { + public string Name { get; init; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + public Guid DepartmentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var department = + await _context.Departments.FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); + + if (department is null) + { + throw new KeyNotFoundException("Department does not exists."); + } + + var room = await _context.Rooms.FirstOrDefaultAsync(r => + r.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); + + if (room is not null) + { + throw new ConflictException("Room name already exists."); + } + + var entity = new Room + { + Name = request.Name.Trim(), + Description = request.Description?.Trim(), + NumberOfLockers = 0, + Capacity = request.Capacity, + Department = department, + }; + var result = await _context.Rooms.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs b/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs deleted file mode 100644 index f255b988..00000000 --- a/src/Application/Rooms/Commands/AddRoom/AddRoomCommand.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands.AddRoom; - -public record AddRoomCommand : IRequest -{ - public string Name { get; init; } = null!; - public string? Description { get; init; } - public int Capacity { get; init; } - -} - -public class AddRoomCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public AddRoomCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(AddRoomCommand request, CancellationToken cancellationToken) - { - - var room = await _context.Rooms.FirstOrDefaultAsync(r => - r.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()), cancellationToken); - - if (room is not null) - { - throw new ConflictException("Room name already exists."); - } - - var entity = new Room - { - Name = request.Name.Trim(), - Description = request.Description?.Trim(), - NumberOfLockers = 0, - Capacity = request.Capacity - }; - var result = await _context.Rooms.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs b/src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs deleted file mode 100644 index bfa65d1a..00000000 --- a/src/Application/Rooms/Commands/AddRoom/AddRoomCommandValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Application.Common.Interfaces; -using FluentValidation; - -namespace Application.Rooms.Commands.AddRoom; - -public class AddRoomCommandValidator : AbstractValidator -{ - private readonly IApplicationDbContext _context; - public AddRoomCommandValidator(IApplicationDbContext context) - { - _context = context; - - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.Capacity) - .GreaterThanOrEqualTo(1).WithMessage("Room's capacity cannot be less than 1"); - - RuleFor(x => x.Name) - .NotEmpty().WithMessage("Name is required.") - .MaximumLength(64).WithMessage("Name cannot exceed 64 characters.") - .Must(BeUnique).WithMessage("Room name already exists."); - - RuleFor(x => x.Description) - .NotEmpty().WithMessage("Description is required.") - .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); - } - - private bool BeUnique(string name) - { - return _context.Rooms.FirstOrDefault(x => x.Name.Equals(name)) is null; - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/DisableRoom.cs b/src/Application/Rooms/Commands/DisableRoom.cs new file mode 100644 index 00000000..580b62c6 --- /dev/null +++ b/src/Application/Rooms/Commands/DisableRoom.cs @@ -0,0 +1,84 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Commands; + +public class DisableRoom +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .NotEmpty().WithMessage("RoomId is required."); + } + } + + public record Command : IRequest + { + public Guid RoomId { get; init; } + } + + public class DisableRoomCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public DisableRoomCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .Include(x => x.Lockers) + .ThenInclude(y => y.Folders) + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + if (!room.IsAvailable) + { + throw new ConflictException("Room have already been disabled."); + } + + var canNotDisable = await _context.Documents + .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken) + > 0; + + if (canNotDisable) + { + throw new InvalidOperationException("Room cannot be disabled because it contains documents."); + } + + var lockers = _context.Lockers.Include(x=> x.Folders) + .Where(x => x.Room.Id.Equals(room.Id)); + + foreach (var locker in lockers) + { + foreach (var folder in locker.Folders) + { + folder.IsAvailable = false; + } + locker.IsAvailable = false; + } + _context.Lockers.UpdateRange(lockers); + room.IsAvailable = false; + var result = _context.Rooms.Update(room); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs b/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs deleted file mode 100644 index 91ca9bdb..00000000 --- a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommand.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands.DisableRoom; - -public record DisableRoomCommand : IRequest -{ - public Guid RoomId { get; init; } -} - -public class DisableRoomCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public DisableRoomCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(DisableRoomCommand request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .Include(x => x.Lockers) - .ThenInclude(y => y.Folders) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (!room.IsAvailable) - { - throw new ConflictException("Room have already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Room cannot be disabled because it contains documents."); - } - - var lockers = _context.Lockers.Include(x=> x.Folders) - .Where(x => x.Room.Id.Equals(room.Id)); - - foreach (var locker in lockers) - { - foreach (var folder in locker.Folders) - { - folder.IsAvailable = false; - } - locker.IsAvailable = false; - } - _context.Lockers.UpdateRange(lockers); - room.IsAvailable = false; - var result = _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommandValidator.cs b/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommandValidator.cs deleted file mode 100644 index de317f88..00000000 --- a/src/Application/Rooms/Commands/DisableRoom/DisableRoomCommandValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentValidation; - -namespace Application.Rooms.Commands.DisableRoom; - -public class DisableRoomCommandValidator : AbstractValidator -{ - public DisableRoomCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/EnableRoom.cs b/src/Application/Rooms/Commands/EnableRoom.cs new file mode 100644 index 00000000..0e0cf048 --- /dev/null +++ b/src/Application/Rooms/Commands/EnableRoom.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Commands; + +public class EnableRoom +{ + public record Command : IRequest + { + public Guid RoomId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs b/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs deleted file mode 100644 index d89c52a5..00000000 --- a/src/Application/Rooms/Commands/EnableRoom/EnableRoomCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Rooms.Commands.EnableRoom; - -public record EnableRoomCommand : IRequest -{ - public Guid RoomId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs new file mode 100644 index 00000000..ef079f47 --- /dev/null +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -0,0 +1,63 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Commands; + +public class RemoveRoom +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .NotEmpty().WithMessage("RoomId is required."); + } + } + + public record Command : IRequest + { + public Guid RoomId { get; init; } + } + + public class RemoveRoomCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public RemoveRoomCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var canNotRemove = await _context.Documents + .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken) + > 0; + + if (canNotRemove) + { + throw new InvalidOperationException("Room cannot be removed because it contains documents."); + } + + var result = _context.Rooms.Remove(room); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommand.cs b/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommand.cs deleted file mode 100644 index 43e7f575..00000000 --- a/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommand.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands.RemoveRoom; - -public record RemoveRoomCommand : IRequest -{ - public Guid RoomId { get; init; } -} - -public class RemoveRoomCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public RemoveRoomCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(RemoveRoomCommand request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - var canNotRemove = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken) - > 0; - - if (canNotRemove) - { - throw new InvalidOperationException("Room cannot be removed because it contains documents."); - } - - room.IsAvailable = false; - var result = _context.Rooms.Remove(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommandValidator.cs b/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommandValidator.cs deleted file mode 100644 index d138ba88..00000000 --- a/src/Application/Rooms/Commands/RemoveRoom/RemoveRoomCommandValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using FluentValidation; - -namespace Application.Rooms.Commands.RemoveRoom; - -public class RemoveRoomCommandValidator : AbstractValidator -{ - public RemoveRoomCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs new file mode 100644 index 00000000..3dc20417 --- /dev/null +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -0,0 +1,15 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Commands; + +public class UpdateRoom +{ + public record Command : IRequest + { + public Guid RoomId { get; init; } + public string Name { get; set; } = null!; + public string? Description { get; init; } + public int Capacity { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs b/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs deleted file mode 100644 index 2e603d9d..00000000 --- a/src/Application/Rooms/Commands/UpdateRoom/UpdateRoomCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Rooms.Commands.UpdateRoom; - -public record UpdateRoomCommand : IRequest -{ - public Guid RoomId { get; init; } - public string Name { get; set; } = null!; - public string? Description { get; init; } - public int Capacity { get; init; } -} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs b/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs deleted file mode 100644 index c691570f..00000000 --- a/src/Application/Rooms/Queries/GetAllRoomPaginated/GetAllRoomsPaginatedQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Rooms.Queries.GetAllRoomPaginated; - -public record GetAllRoomsPaginatedQuery : IRequest> -{ - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } -} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs new file mode 100644 index 00000000..8094f631 --- /dev/null +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -0,0 +1,16 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Queries; + +public class GetAllRoomsPaginated +{ + public record Query : IRequest> + { + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs new file mode 100644 index 00000000..3534dacb --- /dev/null +++ b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs @@ -0,0 +1,54 @@ +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetEmptyContainersPaginated +{ + public record Query : IRequest> + { + public Guid RoomId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var pageNumber = request.Page ?? 1; + var sizeNumber = request.Size ?? 5; + var lockers = _context.Lockers + .Where(x => x.Room.Id == request.RoomId + && x.IsAvailable + && x.Folders.Any(y => y.Capacity > y.NumberOfDocuments && y.IsAvailable)) + .ProjectTo(_mapper.ConfigurationProvider) + .AsEnumerable() + .ToList(); + + lockers.ForEach(x => x.Folders = x.Folders.Where(y => y.Slot > 0)); + + var result = new PaginatedList(lockers.ToList(), lockers.Count, pageNumber, sizeNumber); + return result; + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs b/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs deleted file mode 100644 index 166881aa..00000000 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated/GetEmptyContainersPaginatedQuery.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Queries.GetEmptyContainersPaginated; - -public record GetEmptyContainersPaginatedQuery : IRequest> -{ - public Guid RoomId { get; init; } - public int Page { get; init; } - public int Size { get; init; } -} - -public class GetEmptyContainersPaginatedQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public GetEmptyContainersPaginatedQueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(GetEmptyContainersPaginatedQuery request, CancellationToken cancellationToken) - { - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - var lockers = _context.Lockers - .Where(x => x.Room.Id == request.RoomId - && x.IsAvailable - && x.Folders.Any(y => y.Capacity > y.NumberOfDocuments && y.IsAvailable)) - .ProjectTo(_mapper.ConfigurationProvider) - .AsEnumerable() - .ToList(); - - lockers.ForEach(x => x.Folders = x.Folders.Where(y => y.Slot > 0)); - - var result = new PaginatedList(lockers.ToList(), lockers.Count(), request.Page, request.Size); - return result; - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs new file mode 100644 index 00000000..6a3657bd --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Rooms.Queries; + +public class GetRoomById +{ + public record Query : IRequest + { + public Guid RoomId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs b/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs deleted file mode 100644 index 638e9b51..00000000 --- a/src/Application/Rooms/Queries/GetRoomById/GetRoomByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Models.Dtos.Physical; -using MediatR; - -namespace Application.Rooms.Queries.GetRoomById; - -public record GetRoomByIdQuery : IRequest -{ - public Guid RoomId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs new file mode 100644 index 00000000..c3415e00 --- /dev/null +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -0,0 +1,51 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Staffs.Commands; + +public class AddStaff +{ + public record Command : IRequest + { + public Guid UserId { get; init; } + public Guid? RoomId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + + var staff = new Staff + { + Id = user.Id, + User = user, + Room = room + }; + + var result = await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs b/src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs deleted file mode 100644 index 409e8cf9..00000000 --- a/src/Application/Staffs/Commands/AddStaff/AddStaffCommand.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Application.Common.Interfaces; -using Application.Users.Queries.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Staffs.Commands.AddStaff; - -public record AddStaffCommand : IRequest -{ - public Guid UserId { get; init; } - public Guid RoomId { get; init; } -} - -public class AddStaffCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public AddStaffCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(AddStaffCommand request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - var staff = new Staff - { - Id = user.Id, - User = user, - Room = room - }; - - var result = await _context.Staffs.AddAsync(staff, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs new file mode 100644 index 00000000..607eb057 --- /dev/null +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Staffs.Commands; + +public class RemoveStaffFromRoom +{ + public record Command : IRequest + { + public Guid StaffId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs deleted file mode 100644 index c25f18ca..00000000 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom/RemoveStaffFromRoomCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Application.Users.Queries.Physical; -using MediatR; - -namespace Application.Staffs.Commands.RemoveStaffFromRoom; - -public record RemoveStaffFromRoomCommand : IRequest -{ - public Guid StaffId { get; init; } - public Guid RoomId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs new file mode 100644 index 00000000..270dfe77 --- /dev/null +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -0,0 +1,17 @@ +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Staffs.Queries; + +public class GetAllStaffsPaginated +{ + public class Query : IRequest> + { + public string? SearchTerm { get; set; } + public int? Page { get; set; } + public int? Size { get; set; } + public string? SortBy { get; set; } + public string? SortOrder { get; set; } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs deleted file mode 100644 index a640b907..00000000 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated/GetAllStaffsPaginatedQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Common.Models; -using Application.Users.Queries.Physical; -using MediatR; - -namespace Application.Staffs.Queries.GetAllStaffsPaginated; - -public class GetAllStaffsPaginatedQuery : IRequest> -{ - public string? SearchTerm { get; set; } - public int? Page { get; set; } - public int? Size { get; set; } - public string? SortBy { get; set; } - public string? SortOrder { get; set; } -} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffById.cs b/src/Application/Staffs/Queries/GetStaffById.cs new file mode 100644 index 00000000..7510235e --- /dev/null +++ b/src/Application/Staffs/Queries/GetStaffById.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Staffs.Queries; + +public class GetStaffById +{ + public record Query : IRequest + { + public Guid StaffId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs b/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs deleted file mode 100644 index 7efed7f2..00000000 --- a/src/Application/Staffs/Queries/GetStaffById/GetStaffByIdQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Users.Queries.Physical; -using MediatR; - -namespace Application.Staffs.Queries.GetStaffById; - -public record GetStaffByIdQuery : IRequest -{ - public Guid StaffId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffByRoom.cs b/src/Application/Staffs/Queries/GetStaffByRoom.cs new file mode 100644 index 00000000..9f1ac07b --- /dev/null +++ b/src/Application/Staffs/Queries/GetStaffByRoom.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Dtos.Physical; +using MediatR; + +namespace Application.Staffs.Queries; + +public class GetStaffByRoom +{ + public record Query : IRequest + { + public Guid RoomId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs b/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs deleted file mode 100644 index 0d3d8a1f..00000000 --- a/src/Application/Staffs/Queries/GetStaffByRoom/GetStaffByRoomQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Users.Queries.Physical; -using MediatR; - -namespace Application.Staffs.Queries.GetStaffByRoom; - -public record GetStaffByRoomQuery : IRequest -{ - public Guid RoomId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs new file mode 100644 index 00000000..448b8c08 --- /dev/null +++ b/src/Application/Users/Commands/AddUser.cs @@ -0,0 +1,118 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Helpers; +using Application.Identity; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities; +using Domain.Events; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Users.Commands; + +public class AddUser +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Username) + .NotEmpty().WithMessage("Username is required.") + .MaximumLength(50).WithMessage("Username cannot exceed 64 characters."); + + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Require valid email.") + .MaximumLength(320).WithMessage("Email length too long."); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("Password is required."); + + RuleFor(x => x.Role) + .NotEmpty().WithMessage("Role is required.") + .MaximumLength(64).WithMessage("Role cannot exceed 64 characters.") + .Must(BeNotAdmin).WithMessage("Cannot add a user as Administrator."); + + RuleFor(x => x.FirstName) + .MaximumLength(50).WithMessage("First name cannot exceed 50 characters."); + + RuleFor(x => x.LastName) + .MaximumLength(50).WithMessage("Last name cannot exceed 50 characters."); + + RuleFor(x => x.Position) + .MaximumLength(64).WithMessage("Position cannot exceed 64 characters."); + } + + private static bool BeNotAdmin(string role) + { + return !role.Equals(IdentityData.Roles.Admin); + } + } + + public record Command : IRequest + { + public string Username { get; init; } = null!; + public string Email { get; init; } = null!; + public string Password { get; init; } = null!; + public string? FirstName { get; init; } + public string? LastName { get; init; } + public Guid DepartmentId { get; init; } + public string Role { get; init; } = null!; + public string? Position { get; init; } + } + + public class AddUserCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync( + x => x.Username.Equals(request.Username) || x.Email.Equals(request.Email), cancellationToken); + + if (user is not null) + { + throw new ConflictException("Username or Email has been taken."); + } + + var department = await _context.Departments + .FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); + + if (department is null) + { + throw new KeyNotFoundException("Department does not exist."); + } + + var entity = new User + { + Username = request.Username, + PasswordHash = SecurityUtil.Hash(request.Password), + Email = request.Email, + FirstName = request.FirstName?.Trim(), + LastName = request.LastName?.Trim(), + Department = department, + Role = request.Role, + Position = request.Position, + IsActive = true, + IsActivated = false, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow) + }; + entity.AddDomainEvent(new UserCreatedEvent(entity)); + var result = await _context.Users.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser/AddUserCommand.cs b/src/Application/Users/Commands/AddUser/AddUserCommand.cs deleted file mode 100644 index 5f5f684c..00000000 --- a/src/Application/Users/Commands/AddUser/AddUserCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Helpers; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities; -using Domain.Events; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Users.Commands.AddUser; - -public record AddUserCommand : IRequest -{ - public string Username { get; init; } = null!; - public string Email { get; init; } = null!; - public string Password { get; init; } = null!; - public string? FirstName { get; init; } - public string? LastName { get; init; } - public Guid DepartmentId { get; init; } - public string Role { get; init; } = null!; - public string? Position { get; init; } -} - -public class AddUserCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public async Task Handle(AddUserCommand request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync( - x => x.Username.Equals(request.Username) || x.Email.Equals(request.Email), cancellationToken); - - if (user is not null) - { - throw new ConflictException("Username or Email has been taken."); - } - - var department = await _context.Departments - .FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); - - if (department is null) - { - throw new KeyNotFoundException("Department does not exist."); - } - - var entity = new User - { - Username = request.Username, - PasswordHash = SecurityUtil.Hash(request.Password), - Email = request.Email, - FirstName = request.FirstName?.Trim(), - LastName = request.LastName?.Trim(), - Department = department, - Role = request.Role, - Position = request.Position, - IsActive = true, - IsActivated = false, - Created = LocalDateTime.FromDateTime(DateTime.UtcNow) - }; - entity.AddDomainEvent(new UserCreatedEvent(entity)); - var result = await _context.Users.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} - diff --git a/src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs b/src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs deleted file mode 100644 index 16d07c05..00000000 --- a/src/Application/Users/Commands/AddUser/AddUserCommandValidator.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Identity; -using FluentValidation; - -namespace Application.Users.Commands.AddUser; - -public class AddUserCommandValidator : AbstractValidator -{ - public AddUserCommandValidator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.Username) - .NotEmpty().WithMessage("Username is required.") - .MaximumLength(50).WithMessage("Username cannot exceed 64 characters."); - - RuleFor(x => x.Email) - .NotEmpty().WithMessage("Email is required.") - .EmailAddress().WithMessage("Require valid email.") - .MaximumLength(320).WithMessage("Email length too long."); - - RuleFor(x => x.Password) - .NotEmpty().WithMessage("Password is required."); - - RuleFor(x => x.Role) - .NotEmpty().WithMessage("Role is required.") - .MaximumLength(64).WithMessage("Role cannot exceed 64 characters.") - .Must(BeNotAdmin).WithMessage("Cannot add a user as Administrator."); - - RuleFor(x => x.FirstName) - .MaximumLength(50).WithMessage("First name cannot exceed 50 characters."); - - RuleFor(x => x.LastName) - .MaximumLength(50).WithMessage("Last name cannot exceed 50 characters."); - - RuleFor(x => x.Position) - .MaximumLength(64).WithMessage("Position cannot exceed 64 characters."); - } - - private static bool BeNotAdmin(string role) - { - return !role.Equals(IdentityData.Roles.Admin); - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/DisableUser.cs b/src/Application/Users/Commands/DisableUser.cs new file mode 100644 index 00000000..dafb4b31 --- /dev/null +++ b/src/Application/Users/Commands/DisableUser.cs @@ -0,0 +1,47 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Users.Queries; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Commands; + +public class DisableUser +{ + public record Command : IRequest + { + public Guid UserId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + if (!user.IsActive) + { + throw new ConflictException("User has already been disabled."); + } + + user.IsActive = false; + + var result = _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs b/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs deleted file mode 100644 index 43abac74..00000000 --- a/src/Application/Users/Commands/DisableUser/DisableUserCommand.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Users.Queries; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Users.Commands.DisableUser; - -public record DisableUserCommand : IRequest -{ - public Guid UserId { get; init; } -} - -public class DisableUserCommandHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public DisableUserCommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(DisableUserCommand request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - if (!user.IsActive) - { - throw new ConflictException("User has already been disabled."); - } - - user.IsActive = false; - - var result = _context.Users.Update(user); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/EnableUser.cs b/src/Application/Users/Commands/EnableUser.cs new file mode 100644 index 00000000..7da8ab48 --- /dev/null +++ b/src/Application/Users/Commands/EnableUser.cs @@ -0,0 +1,12 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Users.Commands; + +public class EnableUser +{ + public record Command : IRequest + { + public Guid UserId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs b/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs deleted file mode 100644 index 047f13d5..00000000 --- a/src/Application/Users/Commands/EnableUser/EnableUserCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Users.Commands.EnableUser; - -public record EnableUserCommand : IRequest -{ - public Guid UserId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs new file mode 100644 index 00000000..4d894cc0 --- /dev/null +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -0,0 +1,16 @@ +using Application.Users.Queries; +using MediatR; + +namespace Application.Users.Commands; + +public class UpdateUser +{ + public record Command : IRequest + { + public Guid UserId { get; init; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public string Role { get; init; } = null!; + public string? Position { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs b/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs deleted file mode 100644 index 0f3736ea..00000000 --- a/src/Application/Users/Commands/UpdateUser/UpdateUserCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Users.Commands.UpdateUser; - -public record UpdateUserCommand : IRequest -{ - public Guid UserId { get; init; } - public string Username { get; init; } = null!; - public string Email { get; init; } = null!; - public string? FirstName { get; init; } - public string? LastName { get; init; } - public string Role { get; init; } = null!; - public string? Position { get; init; } -} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs new file mode 100644 index 00000000..738ed5ea --- /dev/null +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -0,0 +1,17 @@ +using Application.Common.Models; +using MediatR; + +namespace Application.Users.Queries; + +public class GetAllUsersPaginated +{ + public record Query : IRequest> + { + public Guid? DepartmentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs b/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs deleted file mode 100644 index 0c3146c7..00000000 --- a/src/Application/Users/Queries/GetAllUsersPaginated/GetAllUsersPaginatedQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Common.Models; -using MediatR; - -namespace Application.Users.Queries.GetAllUsersPaginated; - -public record GetAllUsersPaginatedQuery : IRequest> -{ - public Guid? DepartmentId { get; init; } - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } -} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs new file mode 100644 index 00000000..e34b58d2 --- /dev/null +++ b/src/Application/Users/Queries/GetUserById.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace Application.Users.Queries; + +public class GetUserById +{ + public record Query : IRequest + { + public Guid UserId { get; init; } + } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs b/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs deleted file mode 100644 index 9f2e60d7..00000000 --- a/src/Application/Users/Queries/GetUserById/GetUserByIdQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace Application.Users.Queries.GetUserById; - -public record GetUserByIdQuery : IRequest -{ - public Guid UserId { get; init; } -} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUsersByName/GetUsersByNameQuery.cs b/src/Application/Users/Queries/GetUsersByName/GetUsersByNameQuery.cs deleted file mode 100644 index 76604efb..00000000 --- a/src/Application/Users/Queries/GetUsersByName/GetUsersByNameQuery.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Mappings; -using Application.Common.Models; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using MediatR; - -namespace Application.Users.Queries.GetUsersByName; - -public record GetUsersByNameQuery : IRequest> -{ - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } -} - -public class GetUsersByNameQueryHandler : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public GetUsersByNameQueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(GetUsersByNameQuery request, CancellationToken cancellationToken) - { - var pageNumber = request.Page ?? 1; - var sizeNumber = request.Size ?? 5; - var users = await _context.Users - .Where(x => string.IsNullOrEmpty(request.SearchTerm) - || x.FirstName.ToLower().Contains(request.SearchTerm.ToLower()) - || x.LastName.ToLower().Contains(request.SearchTerm.ToLower())) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(x => x.Username) - .PaginatedListAsync(pageNumber, sizeNumber); - return users; - } -} \ No newline at end of file diff --git a/src/Domain/Entities/Department.cs b/src/Domain/Entities/Department.cs index 6513aa25..b43bbfe7 100644 --- a/src/Domain/Entities/Department.cs +++ b/src/Domain/Entities/Department.cs @@ -1,8 +1,10 @@ using Domain.Common; +using Domain.Entities.Physical; namespace Domain.Entities; public class Department : BaseEntity { public string Name { get; set; } = null!; + public Room? Room { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Room.cs b/src/Domain/Entities/Physical/Room.cs index 517ab146..3d2318ac 100644 --- a/src/Domain/Entities/Physical/Room.cs +++ b/src/Domain/Entities/Physical/Room.cs @@ -7,10 +7,12 @@ public class Room : BaseEntity public string Name { get; set; } = null!; public string? Description { get; set; } public Staff? Staff { get; set; } + public Guid DepartmentId { get; set; } public int Capacity { get; set; } public int NumberOfLockers { get; set; } public bool IsAvailable { get; set; } // Navigation property + public Department Department { get; set; } = null!; public ICollection Lockers { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Staff.cs b/src/Domain/Entities/Physical/Staff.cs index 1184a040..bb6f2a28 100644 --- a/src/Domain/Entities/Physical/Staff.cs +++ b/src/Domain/Entities/Physical/Staff.cs @@ -5,5 +5,5 @@ namespace Domain.Entities.Physical; public class Staff : BaseEntity { public User User { get; set; } = null!; - public Room Room { get; set; } = null!; + public Room? Room { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index 210a75a5..cc8c2345 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -6,7 +6,7 @@ namespace Domain.Entities; public class User : BaseAuditableEntity { public string Username { get; set; } = null!; - public string? Email { get; set; } + public string Email { get; set; } = null!; public string PasswordHash { get; set; } = null!; public string? FirstName { get; set; } public string? LastName { get; set; } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 911a81f6..8537748f 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -21,8 +21,4 @@ - - - - diff --git a/src/Infrastructure/Persistence/Configurations/DepartmentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DepartmentConfiguration.cs index 8af33bea..1c7f00ca 100644 --- a/src/Infrastructure/Persistence/Configurations/DepartmentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DepartmentConfiguration.cs @@ -1,4 +1,5 @@ using Domain.Entities; +using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.ValueGeneration; diff --git a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs index 636a330b..2ad65961 100644 --- a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs @@ -1,3 +1,4 @@ +using Domain.Entities; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Infrastructure/Persistence/Configurations/StaffConfiguration.cs b/src/Infrastructure/Persistence/Configurations/StaffConfiguration.cs index 316bf414..b2881567 100644 --- a/src/Infrastructure/Persistence/Configurations/StaffConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/StaffConfiguration.cs @@ -22,6 +22,6 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(x => x.Room) .WithOne(x => x.Staff) .HasForeignKey("RoomId") - .IsRequired(); + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs index a12b422f..b3e55685 100644 --- a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -19,7 +19,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Email) .HasMaxLength(320) - .IsRequired(false); + .IsRequired(); builder.Property(x => x.PasswordHash) .HasMaxLength(64) diff --git a/src/Infrastructure/Persistence/Migrations/20230524080300_Add_Refresh_Token.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000007_Add_Refresh_Token.Designer.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230524080300_Add_Refresh_Token.Designer.cs rename to src/Infrastructure/Persistence/Migrations/00000000000007_Add_Refresh_Token.Designer.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230524080300_Add_Refresh_Token.cs b/src/Infrastructure/Persistence/Migrations/00000000000007_Add_Refresh_Token.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230524080300_Add_Refresh_Token.cs rename to src/Infrastructure/Persistence/Migrations/00000000000007_Add_Refresh_Token.cs diff --git a/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.Designer.cs new file mode 100644 index 00000000..1ff0d731 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.Designer.cs @@ -0,0 +1,475 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230527234317_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship")] + partial class UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Department") + .HasForeignKey("Domain.Entities.Department", "RoomId"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Department"); + + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.cs b/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.cs new file mode 100644 index 00000000..6bd9968b --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000008_UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship.cs @@ -0,0 +1,123 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UpdateRoomAndStaffRelationshipAndRoomAndDepartmentRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Staffs_Rooms_RoomId", + table: "Staffs"); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Users", + type: "character varying(320)", + maxLength: 320, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(320)", + oldMaxLength: 320, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "RoomId", + table: "Staffs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "DepartmentId", + table: "Rooms", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "RoomId", + table: "Departments", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Departments_RoomId", + table: "Departments", + column: "RoomId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments", + column: "RoomId", + principalTable: "Rooms", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Staffs_Rooms_RoomId", + table: "Staffs", + column: "RoomId", + principalTable: "Rooms", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments"); + + migrationBuilder.DropForeignKey( + name: "FK_Staffs_Rooms_RoomId", + table: "Staffs"); + + migrationBuilder.DropIndex( + name: "IX_Departments_RoomId", + table: "Departments"); + + migrationBuilder.DropColumn( + name: "DepartmentId", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "RoomId", + table: "Departments"); + + migrationBuilder.AlterColumn( + name: "Email", + table: "Users", + type: "character varying(320)", + maxLength: 320, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(320)", + oldMaxLength: 320); + + migrationBuilder.AlterColumn( + name: "RoomId", + table: "Staffs", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Staffs_Rooms_RoomId", + table: "Staffs", + column: "RoomId", + principalTable: "Rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs new file mode 100644 index 00000000..a20e242e --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs @@ -0,0 +1,477 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230528182741_RoomMustHaveADepartment")] + partial class RoomMustHaveADepartment + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs new file mode 100644 index 00000000..a9fa4a85 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RoomMustHaveADepartment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index a69d09d2..49740741 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -189,6 +189,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("DepartmentId") + .HasColumnType("uuid"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -208,6 +211,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasAlternateKey("Name"); + b.HasIndex("DepartmentId") + .IsUnique(); + b.ToTable("Rooms"); }); @@ -218,7 +224,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("UserId"); - b.Property("RoomId") + b.Property("RoomId") .HasColumnType("uuid"); b.HasKey("Id"); @@ -281,6 +287,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Email") + .IsRequired() .HasMaxLength(320) .HasColumnType("character varying(320)"); @@ -392,6 +399,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Room"); }); + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => { b.HasOne("Domain.Entities.User", "User") @@ -402,9 +420,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Domain.Entities.Physical.Room", "Room") .WithOne("Staff") - .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); b.Navigation("Room"); @@ -431,6 +447,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); }); + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => { b.Navigation("Documents"); diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 7e0d22c0..370fbc40 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -1,13 +1,13 @@ -using Application.Departments.Commands.AddDepartment; +using Application.Helpers; using Bogus; using Domain.Common; using Domain.Entities; using Domain.Entities.Physical; -using FluentAssertions; using Infrastructure.Persistence; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using NodaTime; using Xunit; namespace Application.Tests.Integration; @@ -15,19 +15,16 @@ namespace Application.Tests.Integration; [Collection(nameof(BaseCollectionFixture))] public class BaseClassFixture { - protected readonly Faker _departmentGenerator = new Faker() - .RuleFor(x => x.Name, faker => faker.Commerce.Department()); - - protected static IServiceScopeFactory _scopeFactory = null!; + protected static IServiceScopeFactory ScopeFactory = null!; protected BaseClassFixture(CustomApiFactory apiFactory) { - _scopeFactory = apiFactory.Services.GetRequiredService(); + ScopeFactory = apiFactory.Services.GetRequiredService(); } protected static async Task SendAsync(IRequest request) { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); @@ -36,7 +33,7 @@ protected static async Task SendAsync(IRequest protected void Remove(TEntity entity) where TEntity : BaseEntity? { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); @@ -47,7 +44,7 @@ protected void Remove(TEntity entity) where TEntity : BaseEntity? protected static async Task FindAsync(params object[] keyValues) where TEntity : class { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); @@ -57,7 +54,7 @@ protected void Remove(TEntity entity) where TEntity : BaseEntity? protected static async Task AddAsync(TEntity entity) where TEntity : class { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); @@ -69,7 +66,7 @@ protected static async Task AddAsync(TEntity entity) protected static async Task Add(TEntity entity) where TEntity : class { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); @@ -80,7 +77,7 @@ protected static async Task Add(TEntity entity) protected static async Task CountAsync() where TEntity : class { - using var scope = _scopeFactory.CreateScope(); + using var scope = ScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); @@ -142,7 +139,7 @@ protected Locker CreateLocker(params Folder[] folders) return locker; } - protected Room CreateRoom(params Locker[] lockers) + protected Room CreateRoom(Department department, params Locker[] lockers) { var room = new Room() { @@ -150,7 +147,9 @@ protected Room CreateRoom(params Locker[] lockers) Name = new Faker().Commerce.ProductName(), Capacity = 3, NumberOfLockers = lockers.Length, - IsAvailable = true + IsAvailable = true, + Department = department, + DepartmentId = department.Id, }; foreach (var locker in lockers) @@ -160,4 +159,41 @@ protected Room CreateRoom(params Locker[] lockers) return room; } + + protected static Department CreateDepartment() + { + return new Department() + { + Id = Guid.NewGuid(), + Name = new Faker().Random.Word() + }; + } + + protected static User CreateUser(string role, string password) + { + return new User() + { + Id = Guid.NewGuid(), + Username = new Faker().Person.UserName, + Email = new Faker().Person.Email, + FirstName = new Faker().Person.FirstName, + LastName = new Faker().Person.LastName, + Role = role, + Position = new Faker().Random.Word(), + IsActivated = true, + IsActive = true, + Created = LocalDateTime.FromDateTime(DateTime.Now), + PasswordHash = SecurityUtil.Hash(password) + }; + } + + protected static Staff CreateStaff(User user, Room? room) + { + return new Staff() + { + Id = user.Id, + User = user, + Room = room, + }; + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/CustomApiFactory.cs b/tests/Application.Tests.Integration/CustomApiFactory.cs index 9d30c073..f76d4ac3 100644 --- a/tests/Application.Tests.Integration/CustomApiFactory.cs +++ b/tests/Application.Tests.Integration/CustomApiFactory.cs @@ -28,7 +28,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) var databaseSettings = GetConfiguration().GetSection(nameof(DatabaseSettings)).Get(); services.AddDbContext(options => { - options.UseNpgsql(databaseSettings?.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); + options.UseNpgsql(databaseSettings!.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); }); }); } diff --git a/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs b/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs index 8823a9fb..dc3d31e1 100644 --- a/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs +++ b/tests/Application.Tests.Integration/Departments/Commands/AddDepartmentTests.cs @@ -1,5 +1,5 @@ using Application.Common.Exceptions; -using Application.Departments.Commands.DeleteDepartment; +using Application.Departments.Commands; using Domain.Entities; using FluentAssertions; using Xunit; @@ -12,11 +12,14 @@ public AddDepartmentTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact(Timeout = 200)] + [Fact] public async Task ShouldCreateDepartment_WhenDepartmentNameIsValid() { // Arrange - var createDepartmentCommand = _departmentGenerator.Generate(); + var createDepartmentCommand = new AddDepartment.Command() + { + Name = "something", + }; // Act var department = await SendAsync(createDepartmentCommand); @@ -33,11 +36,16 @@ public async Task ShouldCreateDepartment_WhenDepartmentNameIsValid() public async Task ShouldReturnConflict_WhenDepartmentNameHasExisted() { // Arrange - var createDepartmentCommand = _departmentGenerator.Generate(); - var department = await SendAsync(createDepartmentCommand); + var department = CreateDepartment(); + await AddAsync(department); + + var command = new AddDepartment.Command() + { + Name = department.Name, + }; // Act - var action = async () => await SendAsync(createDepartmentCommand); + var action = async () => await SendAsync(command); // Assert await action.Should().ThrowAsync().WithMessage("Department name already exists."); diff --git a/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs b/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs index 6bd82481..f126e367 100644 --- a/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs +++ b/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs @@ -1,6 +1,6 @@ using Application.Common.Mappings; -using Application.Departments.Queries.GetAllDepartments; -using Application.Identity; +using Application.Common.Models.Dtos; +using Application.Departments.Queries; using Application.Users.Queries; using AutoMapper; using Bogus; @@ -23,14 +23,13 @@ public GetAllDepartmentsTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldReturnDepartments_WhenDepartmentsExist() { // Arrange - var department = new Department() { Id = Guid.NewGuid(), Name = new Faker().Commerce.Department() }; await AddAsync(department); - var query = new GetAllDepartmentsQuery(); + var query = new GetAllDepartments.Query(); // Act var result = await SendAsync(query); @@ -46,13 +45,12 @@ public async Task ShouldReturnDepartments_WhenDepartmentsExist() public async Task ShouldReturnEmptyList_WhenNoDepartmentsExist() { // Arrange - var query = new GetAllDepartmentsQuery(); + var query = new GetAllDepartments.Query(); // Act var result = await SendAsync(query); // Assert - result.Count().Should().Be(1); - result.First().Name.Should().Be(IdentityData.Roles.Admin); + result.Count().Should().Be(0); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentTypesTests.cs b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentTypesTests.cs index bf83e5f5..672aec9a 100644 --- a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentTypesTests.cs +++ b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentTypesTests.cs @@ -1,6 +1,4 @@ -using Application.Documents.Queries.GetDocumentTypes; -using Bogus; -using Domain.Entities.Physical; +using Application.Documents.Queries; using FluentAssertions; using Xunit; @@ -16,14 +14,9 @@ public GetAllDocumentTypesTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldReturnDocumentTypes_WhenDocumentTypesExist() { // Arrange - var document = new Document() - { - Id = Guid.NewGuid(), - Title = new Faker().Name.JobTitle(), - DocumentType = new Faker().Commerce.ProductName(), - }; + var document = CreateNDocuments(1).First(); await AddAsync(document); - var query = new GetAllDocumentTypesQuery(); + var query = new GetAllDocumentTypes.Query(); // Act var result = await SendAsync(query); @@ -39,7 +32,7 @@ public async Task ShouldReturnDocumentTypes_WhenDocumentTypesExist() public async Task ShouldReturnEmptyList_WhenNoDocumentTypesExist() { // Arrange - var query = new GetAllDocumentTypesQuery(); + var query = new GetAllDocumentTypes.Query(); // Act var result = await SendAsync(query); diff --git a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs index b051e093..42d3b5d1 100644 --- a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs @@ -1,422 +1,438 @@ - using Application.Common.Exceptions; - using Application.Common.Extensions; - using Application.Common.Mappings; - using Application.Common.Models.Dtos.Physical; - using Application.Documents.Queries.GetAllDocumentsPaginated; - using AutoMapper; - using Bogus; - using Domain.Entities.Physical; - using FluentAssertions; - using Xunit; - - namespace Application.Tests.Integration.Documents.Queries; - - public class GetAllDocumentsPaginatedTests : BaseClassFixture +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Documents.Queries; +using AutoMapper; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Documents.Queries; + +public class GetAllDocumentsPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllDocumentsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) { - private readonly IMapper _mapper; - public GetAllDocumentsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(config => config.AddProfile()); + var configuration = new MapperConfiguration(config => config.AddProfile()); - _mapper = configuration.CreateMapper(); - } + _mapper = configuration.CreateMapper(); + } - [Fact] - public async Task ShouldReturnAllDocuments_WhenNoContainersAreDefined() - { - // Arrange - var documents = CreateNDocuments(1); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(locker); - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery(); - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().ContainEquivalentOf(_mapper.Map(documents.First())); - - // Cleanup - Remove(documents.First()); - Remove(folder); - Remove(locker); - Remove(room); - } - - [Fact] - public async Task ShouldReturnEmptyPaginatedList_WhenNoDocumentsExist() - { - // Arrange - var room = CreateRoom(); + [Fact] + public async Task ShouldReturnAllDocuments_WhenNoContainersAreDefined() + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(1); + documents.First().Department = department; + var folder = CreateFolder(documents); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.First().Title.Should().Be(documents.First().Title); + + // Cleanup + Remove(documents.First()); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldReturnEmptyPaginatedList_WhenNoDocumentsExist() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); - await AddAsync(room); + await AddAsync(room); - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id - }; + var query = new GetAllDocumentsPaginated.Query() + { + RoomId = room.Id + }; - // Act - var result = await SendAsync(query); + // Act + var result = await SendAsync(query); - // Assert - result.Items.Should().BeEmpty(); - - // Cleanup - Remove(room); - } + // Assert + result.Items.Should().BeEmpty(); + + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } - [Fact] - public async Task ShouldReturnDocumentsOfRoom_WhenOnlyRoomIdIsPresent() + [Fact] + public async Task ShouldReturnDocumentsOfRoom_WhenOnlyRoomIdIsPresent() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var documents1 = CreateNDocuments(2); + var folder1 = CreateFolder(documents1); + var locker1 = CreateLocker(folder1); + var room1 = CreateRoom(department1, locker1); + var documents2 = CreateNDocuments(2); + var folder2 = CreateFolder(documents2); + var locker2 = CreateLocker(folder2); + var room2 = CreateRoom(department2, locker2); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var documents1 = CreateNDocuments(2); - - var folder1 = CreateFolder(documents1); - - var locker1 = CreateLocker(folder1); - - var room1 = CreateRoom(locker1); - - var documents2 = CreateNDocuments(2); - - var folder2 = CreateFolder(documents2); - - var locker2 = CreateLocker(folder2); - - var room2 = CreateRoom(locker2); - - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().ContainEquivalentOf(_mapper.Map(documents1[0])); - result.Items.Should().ContainEquivalentOf(_mapper.Map(documents1[1])); - result.Items.Should().NotContainEquivalentOf(_mapper.Map(documents2[0])); - result.Items.Should().NotContainEquivalentOf(_mapper.Map(documents2[1])); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room1); - Remove(room2); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdIsPresentButDoesNotExist() + RoomId = room1.Id + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.Should() + .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); + result.Items.Should() + .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); + + // Cleanup + Remove(documents1[0]); + Remove(documents1[1]); + Remove(documents2[0]); + Remove(documents2[1]); + Remove(folder1); + Remove(folder2); + Remove(locker1); + Remove(locker2); + Remove(room1); + Remove(room2); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdIsPresentButDoesNotExist() + { + // Arrange + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Room does not exist."); - } - - [Fact] - public async Task ShouldReturnDocumentsOfLocker_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsInRoom() + RoomId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync("Room does not exist."); + } + + [Fact] + public async Task ShouldReturnDocumentsOfLocker_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsInRoom() + { + // Arrange + var department = CreateDepartment(); + var documents1 = CreateNDocuments(2); + var folder1 = CreateFolder(documents1); + var locker1 = CreateLocker(folder1); + var documents2 = CreateNDocuments(2); + var folder2 = CreateFolder(documents2); + var locker2 = CreateLocker(folder2); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var documents1 = CreateNDocuments(2); - - var folder1 = CreateFolder(documents1); - - var locker1 = CreateLocker(folder1); - - var documents2 = CreateNDocuments(2); - - var folder2 = CreateFolder(documents2); - - var locker2 = CreateLocker(folder2); - - var room = CreateRoom(locker1, locker2); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id, - LockerId = locker1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().BeEquivalentTo(_mapper.Map>(documents1)); - result.Items.Should().NotContainEquivalentOf(_mapper.Map>(documents2)); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerDoesNotExist() + RoomId = room.Id, + LockerId = locker1.Id + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.Should() + .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); + result.Items.Should() + .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); + + // Cleanup + Remove(documents1[0]); + Remove(documents1[1]); + Remove(documents2[0]); + Remove(documents2[1]); + Remove(folder1); + Remove(folder2); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerDoesNotExist() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var room = CreateRoom(); - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id, - LockerId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Locker does not exist."); - - // Cleanup - Remove(room); - } + RoomId = room.Id, + LockerId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync("Locker does not exist."); - [Fact] - public async Task ShouldThrowConflictException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsNotInRoom() + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsNotInRoom() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var locker = CreateLocker(); + var room1 = CreateRoom(department1); + var room2 = CreateRoom(department2, locker); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var locker = CreateLocker(); - - var room1 = CreateRoom(); - - var room2 = CreateRoom(locker); - - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room1.Id, - LockerId = locker.Id - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Room does not match locker."); - - // Cleanup - Remove(locker); - Remove(room1); - Remove(room2); - } + RoomId = room1.Id, + LockerId = locker.Id + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync("Room does not match locker."); - [Fact] - public async Task ShouldReturnDocumentsOfFolder_WhenAllIdsArePresentAndFolderIsInBothLockerAndRoom() + // Cleanup + Remove(locker); + Remove(room1); + Remove(room2); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); + } + + [Fact] + public async Task ShouldReturnDocumentsOfFolder_WhenAllIdsArePresentAndFolderIsInBothLockerAndRoom() + { + // Arrange + var department = CreateDepartment(); + var documents1 = CreateNDocuments(2); + documents1[0].Department = department; + documents1[1].Department = department; + var folder1 = CreateFolder(documents1); + var documents2 = CreateNDocuments(2); + documents2[0].Department = department; + documents2[1].Department = department; + var folder2 = CreateFolder(documents2); + var locker = CreateLocker(folder1, folder2); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() + { + RoomId = room.Id, + LockerId = locker.Id, + FolderId = folder1.Id + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.Should() + .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); + result.Items.Should() + .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); + + // Cleanup + Remove(documents1[0]); + Remove(documents1[1]); + Remove(documents2[0]); + Remove(documents2[1]); + Remove(folder1); + Remove(folder2); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenAllIdsArePresentAndValidAndFolderDoesNotExist() + { + // Arrange + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); + + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var documents1 = CreateNDocuments(2); - var folder1 = CreateFolder(documents1); - - var documents2 = CreateNDocuments(2); - var folder2 = CreateFolder(documents2); - - var locker = CreateLocker(folder1, folder2); - var room = CreateRoom(locker); - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id, - LockerId = locker.Id, - FolderId = folder1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().BeEquivalentTo(_mapper.Map(documents1)); - result.Items.Should().NotBeEquivalentTo(_mapper.Map(documents2)); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker); - Remove(room); - } + RoomId = room.Id, + LockerId = locker.Id, + FolderId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync("Folder does not exist."); - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenAllIdsArePresentAndValidAndFolderDoesNotExist() + // Cleanup + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenAllIdsArePresentAndFolderIsNotInLockerOrInRoom() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var folder1 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var room1 = CreateRoom(department1, locker1); + var folder2 = CreateFolder(); + var locker2 = CreateLocker(folder2); + var room2 = CreateRoom(department2, locker2); + await AddAsync(room1); + await AddAsync(room2); + + var query1 = new GetAllDocumentsPaginated.Query() { - // Arrange - var locker = CreateLocker(); - var room = CreateRoom(locker); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id, - LockerId = locker.Id, - FolderId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Folder does not exist."); - - // Cleanup - Remove(locker); - Remove(room); - } + RoomId = room1.Id, + LockerId = locker2.Id, + FolderId = folder1.Id + }; - [Fact] - public async Task ShouldThrowConflictException_WhenAllIdsArePresentAndFolderIsNotInLockerOrInRoom() + var query2 = new GetAllDocumentsPaginated.Query() { - // Arrange - var folder1 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var room1 = CreateRoom(locker1); - - var folder2 = CreateFolder(); - var locker2 = CreateLocker(folder2); - var room2 = CreateRoom(locker2); - - await AddAsync(room1); - await AddAsync(room2); - - var query1 = new GetAllDocumentsPaginatedQuery() - { - RoomId = room1.Id, - LockerId = locker2.Id, - FolderId = folder1.Id - }; - - var query2 = new GetAllDocumentsPaginatedQuery() - { - RoomId = room1.Id, - LockerId = locker2.Id, - FolderId = folder2.Id - }; - - // Act - var action1 = async () => await SendAsync(query1); - var action2 = async () => await SendAsync(query2); - - // Assert - await action1.Should().ThrowAsync("Either locker or room does not match folder."); - await action1.Should().ThrowAsync("Either locker or room does not match folder."); - - // Cleanup - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room1); - Remove(room2); - } + RoomId = room1.Id, + LockerId = locker2.Id, + FolderId = folder2.Id + }; + + // Act + var action1 = async () => await SendAsync(query1); + var action2 = async () => await SendAsync(query2); + + // Assert + await action1.Should().ThrowAsync("Either locker or room does not match folder."); + await action2.Should().ThrowAsync("Either locker or room does not match folder."); - [Fact] - public async Task ShouldReturnSortedByIdPaginatedList_WhenSortByIsNotPresent() + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(locker1); + Remove(locker2); + Remove(room1); + Remove(room2); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); + } + + [Fact] + public async Task ShouldReturnSortedByIdPaginatedList_WhenSortByIsNotPresent() + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(2); + var folder = CreateFolder(documents); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var documents = CreateNDocuments(2); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(locker); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.First().Should().BeEquivalentTo(_mapper.Map(documents[0].Id.CompareTo(documents[1].Id) <= 0 ? documents[0] : documents[1])); - - // Cleanup - Remove(documents[0]); - Remove(documents[1]); - Remove(folder); - Remove(locker); - Remove(room); - } - - [Theory] - [InlineData(nameof(DocumentDto.Id), "asc")] - [InlineData(nameof(DocumentDto.Title), "asc")] - [InlineData(nameof(DocumentDto.DocumentType), "asc")] - [InlineData(nameof(DocumentDto.Description), "asc")] - [InlineData(nameof(DocumentDto.Id), "desc")] - [InlineData(nameof(DocumentDto.Title), "desc")] - [InlineData(nameof(DocumentDto.DocumentType), "desc")] - [InlineData(nameof(DocumentDto.Description), "desc")] - public async Task ShouldReturnSortedByPropertyPaginatedList_WhenSortByIsPresent(string sortBy, string sortOrder) + RoomId = room.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.First().Should() + .BeEquivalentTo( + _mapper.Map(documents[0].Id.CompareTo(documents[1].Id) <= 0 ? documents[0] : documents[1]), + x => x.IgnoringCyclicReferences()); + + // Cleanup + Remove(documents[0]); + Remove(documents[1]); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Theory] + [InlineData(nameof(DocumentDto.Id), "asc")] + [InlineData(nameof(DocumentDto.Title), "asc")] + [InlineData(nameof(DocumentDto.DocumentType), "asc")] + [InlineData(nameof(DocumentDto.Description), "asc")] + [InlineData(nameof(DocumentDto.Id), "desc")] + [InlineData(nameof(DocumentDto.Title), "desc")] + [InlineData(nameof(DocumentDto.DocumentType), "desc")] + [InlineData(nameof(DocumentDto.Description), "desc")] + public async Task ShouldReturnSortedByPropertyPaginatedList_WhenSortByIsPresent(string sortBy, string sortOrder) + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(2); + var folder = CreateFolder(documents); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + + await AddAsync(room); + + var query = new GetAllDocumentsPaginated.Query() { - // Arrange - var documents = CreateNDocuments(2); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(locker); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginatedQuery() - { - RoomId = room.Id, - SortBy = sortBy, - SortOrder = sortOrder - }; - - var list = new EnumerableQuery(_mapper.Map>(documents)); - var list2 = list.OrderByCustom(sortBy, sortOrder); - var expected = list2.ToList(); - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().BeEquivalentTo(expected); - - // Cleanup - Remove(documents[0]); - Remove(documents[1]); - Remove(folder); - Remove(locker); - Remove(room); - } - } \ No newline at end of file + RoomId = room.Id, + SortBy = sortBy, + SortOrder = sortOrder + }; + + var list = new EnumerableQuery(_mapper.Map>(documents)); + var list2 = list.OrderByCustom(sortBy, sortOrder); + var expected = list2.ToList(); + + // Act + var result = await SendAsync(query); + + // Assert + result.Items.Should().BeEquivalentTo(expected, x => x.IgnoringCyclicReferences()); + + // Cleanup + Remove(documents[0]); + Remove(documents[1]); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs index 8832e0c3..42601c21 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs @@ -1,9 +1,6 @@ using Application.Common.Exceptions; -using Application.Common.Models.Dtos.Physical; -using Application.Folders.Commands.AddFolder; -using Application.Lockers.Commands.AddLocker; -using Application.Rooms.Commands.AddRoom; -using Bogus; +using Application.Folders.Commands; +using Domain.Entities; using Domain.Entities.Physical; using Domain.Exceptions; using FluentAssertions; @@ -13,20 +10,6 @@ namespace Application.Tests.Integration.Folders.Commands; public class AddFolderTests : BaseClassFixture { - private readonly Faker _folderGenerator = new Faker() - .RuleFor(f => f.Name, faker => faker.Commerce.ProductName()) - .RuleFor(f => f.Description, faker => faker.Commerce.ProductDescription()) - .RuleFor(f => f.Capacity, faker => faker.Random.Int(1,9999)); - - private readonly Faker _roomGenerator = new Faker() - .RuleFor(r => r.Name, faker => faker.Commerce.ProductName()) - .RuleFor(r => r.Description, faker => faker.Commerce.ProductDescription()) - .RuleFor(r => r.Capacity, faker => faker.Random.Int(1,9999)); - - private readonly Faker _lockerGenerator = new Faker() - .RuleFor(l => l.Name, faker => faker.Commerce.ProductName()) - .RuleFor(l => l.Description, faker => faker.Commerce.ProductDescription()) - .RuleFor(l => l.Capacity, faker => faker.Random.Int(1,9999)); public AddFolderTests(CustomApiFactory apiFactory) : base(apiFactory) { } @@ -35,30 +18,25 @@ public AddFolderTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldAddFolder_WhenAddDetailsAreValid() { // Arrange - var addRoomCommand = _roomGenerator.Generate(); - var room = await SendAsync(addRoomCommand); - - var addLockerCommand = _lockerGenerator.Generate(); - addLockerCommand = addLockerCommand with - { - RoomId = room.Id, - Capacity = 1 - }; - var locker = await SendAsync(addLockerCommand); + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); + await AddAsync(room); - var addFolderCommand = _folderGenerator.Generate(); - addFolderCommand = addFolderCommand with + var command = new AddFolder.Command() { LockerId = locker.Id, - Capacity = 1 + Capacity = 1, + Name = "something" }; // Act - var folder = await SendAsync(addFolderCommand); + var folder = await SendAsync(command); + // Assert - folder.Name.Should().Be(addFolderCommand.Name); - folder.Description.Should().Be(addFolderCommand.Description); - folder.Capacity.Should().Be(addFolderCommand.Capacity); + folder.Name.Should().Be(command.Name); + folder.Description.Should().Be(command.Description); + folder.Capacity.Should().Be(command.Capacity); folder.Locker.Id.Should().Be(locker.Id); folder.Locker.NumberOfFolders.Should().Be(locker.NumberOfFolders + 1); folder.NumberOfDocuments.Should().Be(0); @@ -66,182 +44,128 @@ public async Task ShouldAddFolder_WhenAddDetailsAreValid() // Clean up var folderEntity = await FindAsync(folder.Id); - var lockerEntity = await FindAsync(locker.Id); - var roomEntity = await FindAsync(room.Id); Remove(folderEntity); - Remove(lockerEntity); - Remove(roomEntity); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldAddFolder_WhenFoldersHasSameNameButInDifferentLockers() { // Arrange - var sameFolderName = new Faker().Commerce.ProductName(); - var addRoomCommand = _roomGenerator.Generate(); - addRoomCommand = addRoomCommand with - { - Capacity = 2 - }; - - var room = await SendAsync(addRoomCommand); - - var addLockerCommand = _lockerGenerator.Generate(); - - var addLockerACommand = addLockerCommand with - { - RoomId = room.Id, - Capacity = 1 - }; - var addLockerBCommand = addLockerCommand with - { - RoomId = room.Id, - Name = new Faker().Commerce.ProductName(), - Capacity = 1 - }; - - var lockerA = await SendAsync(addLockerACommand); - var lockerB = await SendAsync(addLockerBCommand); - - var addFolderCommand = _folderGenerator.Generate(); - var addFolderCommandForLockerA = addFolderCommand with - { - Name = sameFolderName, - LockerId = lockerA.Id - }; - var addFolderCommandForLockerB = addFolderCommand with + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var command = new AddFolder.Command() { - Name = sameFolderName, - LockerId = lockerB.Id + LockerId = locker2.Id, + Name = folder1.Name, + Capacity = 3, }; - var folderA = await SendAsync(addFolderCommandForLockerA); // Act - var folderB = await SendAsync(addFolderCommandForLockerB); + var folder2 = await SendAsync(command); // Assert - folderA.Locker.Id.Should().NotBe(folderB.Locker.Id); - folderA.Name.Should().Be(folderB.Name); + folder1.Name.Should().Be(folder2.Name); // Cleanup - var folderAEntity = await FindAsync(folderA.Id); - var folderBEntity = await FindAsync(folderB.Id); - var lockerAEntity = await FindAsync(lockerA.Id); - var lockerBEntity = await FindAsync(lockerB.Id); - var roomEntity = await FindAsync(room.Id); - Remove(folderAEntity); - Remove(folderBEntity); - Remove(lockerAEntity); - Remove(lockerBEntity); - Remove(roomEntity); + Remove(folder1); + Remove(await FindAsync(folder2.Id)); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowConflictException_WhenFolderAlreadyExistsInTheSameLocker() { // Arrange - var sameFolderName = new Faker().Commerce.ProductName(); - var addRoomCommand = _roomGenerator.Generate(); - var room = await SendAsync(addRoomCommand); - - var addLockerCommand = _lockerGenerator.Generate(); - addLockerCommand = addLockerCommand with - { - RoomId = room.Id, - Capacity = 2 - }; - var locker = await SendAsync(addLockerCommand); - - var addFolderCommand = _folderGenerator.Generate(); - var addFolderCommandForLockerA = addFolderCommand with - { - Name = sameFolderName, - LockerId = locker.Id - }; - var addFolderCommandForLockerB = addFolderCommand with + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new AddFolder.Command() { - Name = sameFolderName, - LockerId = locker.Id + Name = folder.Name, + LockerId = locker.Id, + Capacity = 3 }; - var folder = await SendAsync(addFolderCommandForLockerA); // Act - var action = async () => await SendAsync(addFolderCommandForLockerB); + var action = async () => await SendAsync(command); // Assert - await action.Should().ThrowAsync().WithMessage("Folder's name already exists."); + await action.Should().ThrowAsync() + .WithMessage("Folder name already exists."); // Cleanup - var folderEntity = await FindAsync(folder.Id); - var lockerEntity = await FindAsync(locker.Id); - var roomEntity = await FindAsync(room.Id); - Remove(folderEntity); - Remove(lockerEntity); - Remove(roomEntity); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() { // Arrange - var addRoomCommand = _roomGenerator.Generate(); - var room = await SendAsync(addRoomCommand); - - var addLockerCommand = _lockerGenerator.Generate(); - addLockerCommand = addLockerCommand with + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker = CreateLocker(folder1, folder2, folder3); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new AddFolder.Command() { - RoomId = room.Id, - Capacity = new Faker().Random.Int(1,10) + Name = "something", + Capacity = 3, + LockerId = locker.Id, + Description = "something else", }; - var locker = await SendAsync(addLockerCommand); - var list = new List(); // Act - var action = async () => - { - - for (var i = 0; i <= locker.Capacity; i++) - { - var addFolderCommand = _folderGenerator.Generate(); - addFolderCommand = addFolderCommand with - { - LockerId = locker.Id - }; - list.Add(await SendAsync(addFolderCommand)); - } - }; + var action = async () => await SendAsync(command); // Assert await action.Should().ThrowAsync() .WithMessage("This locker cannot accept more folders."); // Cleanup - foreach (var f in list) - { - var folderEntity = await FindAsync(f.Id); - Remove(folderEntity); - } - var lockerEntity = await FindAsync(locker.Id); - var roomEntity = await FindAsync(room.Id); - Remove(lockerEntity); - Remove(roomEntity); + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenLockerIdNotExists() { // Arrange - var addFolderCommand = _folderGenerator.Generate(); - addFolderCommand = addFolderCommand with + var command = new AddFolder.Command() { - LockerId = Guid.NewGuid() + LockerId = Guid.NewGuid(), + Name = "something", + Capacity = 3 }; // Act - var folder = async () => await SendAsync(addFolderCommand); + var action = async () => await SendAsync(command); // Assert - await folder.Should().ThrowAsync() + await action.Should().ThrowAsync() .WithMessage("Locker does not exist."); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs index 1dee5f9d..02a9e97b 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs @@ -1,5 +1,5 @@ using Application.Common.Exceptions; -using Application.Folders.Commands.DisableFolder; +using Application.Folders.Commands; using Domain.Entities.Physical; using FluentAssertions; using Xunit; @@ -46,7 +46,7 @@ public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() }; await AddAsync(folder); - var disableFolderCommand = new DisableFolderCommand() + var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id }; @@ -67,7 +67,7 @@ public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() public async Task ShouldThrowKeyNotFoundException_WhenFolderDoesNotExist() { // Arrange - var disableFolderCommand = new DisableFolderCommand() + var disableFolderCommand = new DisableFolder.Command() { FolderId = Guid.NewGuid() }; @@ -113,7 +113,7 @@ public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabl Locker = locker }; await AddAsync(folder); - var disableFolderCommand = new DisableFolderCommand() + var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id }; @@ -173,7 +173,7 @@ public async Task ShouldThrowInvalidOperationException_WhenFolderHasDocuments() }; await AddAsync(document); - var disableFolderCommand = new DisableFolderCommand() + var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id }; diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs index 8f3d30e0..2da1413f 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; -using Application.Lockers.Commands.AddLocker; +using Application.Lockers.Commands; using Bogus; using Domain.Entities.Physical; using Domain.Exceptions; using FluentAssertions; -using MediatR; -using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Application.Tests.Integration.Lockers.Commands; @@ -14,7 +12,6 @@ public class AddLockerTests : BaseClassFixture { public AddLockerTests(CustomApiFactory apiFactory) : base(apiFactory) { - } [Fact] @@ -33,7 +30,7 @@ public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() await AddAsync(room); - var addLockerCommand = new AddLockerCommand() + var addLockerCommand = new AddLocker.Command() { Name = new Faker().Name.JobTitle(), Description = new Faker().Lorem.Sentence(), @@ -77,7 +74,7 @@ public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameR await AddAsync(room); - var addLockerCommand = new AddLockerCommand() + var addLockerCommand = new AddLocker.Command() { Name = new Faker().Name.JobTitle(), Description = new Faker().Lorem.Sentence(), @@ -126,7 +123,7 @@ public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() await AddAsync(room2); - var addLockerCommand = new AddLockerCommand() + var addLockerCommand = new AddLocker.Command() { Name = new Faker().Name.JobTitle(), Description = new Faker().Lorem.Sentence(), @@ -134,7 +131,7 @@ public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() RoomId = room1.Id, }; - var addLockerCommand2 = new AddLockerCommand() + var addLockerCommand2 = new AddLocker.Command() { Name = addLockerCommand.Name, Description = new Faker().Lorem.Sentence(), @@ -181,7 +178,7 @@ public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() await AddAsync(room); - var addLockerCommand = new AddLockerCommand() + var addLockerCommand = new AddLocker.Command() { Name = new Faker().Name.JobTitle(), Description = new Faker().Lorem.Sentence(), @@ -189,7 +186,7 @@ public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() RoomId = room.Id, }; - var addLockerCommand2 = new AddLockerCommand() + var addLockerCommand2 = new AddLocker.Command() { Name = new Faker().Name.JobTitle(), Description = new Faker().Lorem.Sentence(), diff --git a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs index 28538e7b..5cf76d23 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs @@ -1,7 +1,6 @@ using Application.Common.Exceptions; -using Application.Lockers.Commands.AddLocker; -using Application.Lockers.Commands.DisableLocker; -using Bogus; +using Application.Lockers.Commands; +using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; using Xunit; @@ -12,37 +11,18 @@ public class DisableLockerTests : BaseClassFixture { public DisableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) { - } [Fact] public async Task ShouldDisableLocker_WhenLockerExistsAndIsAvailable() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); await AddAsync(room); - var createLockerCommand = new AddLockerCommand() - { - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 2, - RoomId = room.Id, - }; - - var locker = await SendAsync(createLockerCommand); - room.NumberOfLockers += 1; - - var disableLockerCommand = new DisableLockerCommand() + var disableLockerCommand = new DisableLocker.Command() { LockerId = locker.Id, }; @@ -58,15 +38,15 @@ public async Task ShouldDisableLocker_WhenLockerExistsAndIsAvailable() result.NumberOfFolders.Should().Be(locker.NumberOfFolders); // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() { // Arrange - var disableLockerCommand = new DisableLockerCommand() + var disableLockerCommand = new DisableLocker.Command() { LockerId = Guid.NewGuid(), }; @@ -84,28 +64,12 @@ await action.Should() public async Task ShouldThrowConflictException_WhenLockerIsAlreadyDisabled() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); await AddAsync(room); - - var createLockerCommand = new AddLockerCommand() - { - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 2, - RoomId = room.Id, - }; - var locker = await SendAsync(createLockerCommand); - var disableLockerCommand = new DisableLockerCommand() + var disableLockerCommand = new DisableLocker.Command() { LockerId = locker.Id, }; @@ -118,7 +82,7 @@ public async Task ShouldThrowConflictException_WhenLockerIsAlreadyDisabled() await action.Should().ThrowAsync().WithMessage("Locker has already been disabled."); // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); + Remove(room); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs index 7f3a9dbb..b4ec3df7 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs @@ -1,10 +1,6 @@ using Application.Common.Exceptions; -using Application.Lockers.Commands.AddLocker; -using Application.Lockers.Commands.DisableLocker; -using Application.Lockers.Commands.EnableLocker; -using Bogus; -using Domain.Entities.Physical; -using Domain.Exceptions; +using Application.Lockers.Commands; +using Domain.Entities; using FluentAssertions; using Xunit; @@ -21,63 +17,39 @@ public EnableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldEnableLocker_WhenLockerExistsAndIsNotAvailable() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var locker = CreateLocker(); + locker.IsAvailable = false; + var room = CreateRoom(department, locker); await AddAsync(room); - var createLockerCommand = new AddLockerCommand() - { - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 2, - RoomId = room.Id, - }; - - var locker = await SendAsync(createLockerCommand); - - var disableLockerCommand = new DisableLockerCommand() - { - LockerId = locker.Id, - }; - - await SendAsync(disableLockerCommand); - // Act - - var enableLockerCommand = new EnableLockerCommand() + var command = new EnableLocker.Command() { LockerId = locker.Id, }; - var result = await SendAsync(enableLockerCommand); + var result = await SendAsync(command); // Assert result.IsAvailable.Should().BeTrue(); // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); + Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() { // Arrange - var enableLockerCommand = new EnableLockerCommand() + var command = new EnableLocker.Command() { LockerId = Guid.NewGuid(), }; // Act - var action = async () => await SendAsync(enableLockerCommand); + var action = async () => await SendAsync(command); // Assert await action.Should() @@ -89,28 +61,12 @@ await action.Should() public async Task ShouldThrowConflictException_WhenLockerIsAlreadyEnabled() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); await AddAsync(room); - - var createLockerCommand = new AddLockerCommand() - { - Name = new Faker().Commerce.ProductName(), - Description = new Faker().Lorem.Sentence(), - Capacity = 2, - RoomId = room.Id, - }; - var locker = await SendAsync(createLockerCommand); - var enableLockerCommand = new EnableLockerCommand() + var enableLockerCommand = new EnableLocker.Command() { LockerId = locker.Id, }; @@ -124,7 +80,7 @@ await action.Should() .WithMessage("Locker has already been enabled."); // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); + Remove(room); + Remove(await FindAsync(department.Id)); } } diff --git a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs index 9fefabc4..39badcb8 100644 --- a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs @@ -1,12 +1,8 @@ using Application.Common.Exceptions; -using Application.Helpers; -using Application.Lockers.Commands.AddLocker; -using Application.Rooms.Commands.DisableRoom; -using Bogus; +using Application.Rooms.Commands; using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; -using NodaTime; using Xunit; namespace Application.Tests.Integration.Rooms.Commands; @@ -21,12 +17,13 @@ public DisableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldDisableRoom_WhenRoomHaveNoDocument() { // Arrange + var department = CreateDepartment(); var folder = CreateFolder(); var locker = CreateLocker(folder); - var room = CreateRoom(locker); + var room = CreateRoom(department, locker); await AddAsync(room); - var disableRoomCommand = new DisableRoomCommand() + var disableRoomCommand = new DisableRoom.Command() { RoomId = room.Id }; @@ -46,13 +43,14 @@ public async Task ShouldDisableRoom_WhenRoomHaveNoDocument() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() { // Arrange - var disableRoomCommand = new DisableRoomCommand() + var disableRoomCommand = new DisableRoom.Command() { RoomId = Guid.NewGuid() }; @@ -69,14 +67,15 @@ await action.Should().ThrowAsync() public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotEmptyOfDocuments() { // Arrange + var department = CreateDepartment(); var documents = CreateNDocuments(1); var folder = CreateFolder(documents); var locker = CreateLocker(folder); - var room = CreateRoom(locker); + var room = CreateRoom(department, locker); await AddAsync(room); - var disableRoomCommand = new DisableRoomCommand() + var disableRoomCommand = new DisableRoom.Command() { RoomId = room.Id }; @@ -93,17 +92,19 @@ await action.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotAvailable() { // Arrange - var room = CreateRoom(); + var department = CreateDepartment(); + var room = CreateRoom(department); room.IsAvailable = false; await AddAsync(room); - var command = new DisableRoomCommand() + var command = new DisableRoom.Command() { RoomId = room.Id }; @@ -117,5 +118,6 @@ await action.Should().ThrowAsync() // Cleanup Remove(room); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs index 70954ce7..69fff2e8 100644 --- a/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs @@ -1,5 +1,5 @@ -using Application.Rooms.Commands.RemoveRoom; -using Bogus; +using Application.Rooms.Commands; +using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; using Xunit; @@ -16,33 +16,38 @@ public RemoveRoomTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldRemoveRoom_WhenRoomHasNoDocuments() { // Arrange - var room = CreateRoom(); + var department = CreateDepartment(); + var room = CreateRoom(department); await Add(room); - var command = new RemoveRoomCommand() + var command = new RemoveRoom.Command() { RoomId = room.Id }; + // Act - var result = await SendAsync(command); + await SendAsync(command); // Assert var deletedRoom = await FindAsync(room.Id); - result.IsAvailable.Should().BeFalse(); deletedRoom.Should().BeNull(); + + // Cleanup + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowInvalidOperationException_WhenRoomHaveDocuments() { // Arrange + var department = CreateDepartment(); var documents = CreateNDocuments(1); var folder = CreateFolder(documents); var locker = CreateLocker(folder); - var room = CreateRoom(locker); + var room = CreateRoom(department, locker); await AddAsync(room); - var command = new RemoveRoomCommand() + var command = new RemoveRoom.Command() { RoomId = room.Id }; @@ -59,13 +64,14 @@ await action.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() { // Arrange - var command = new RemoveRoomCommand() + var command = new RemoveRoom.Command() { RoomId = Guid.NewGuid() }; diff --git a/tests/Application.Tests.Integration/Rooms/Queries/GetEmptyContainersPaginatedTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetEmptyContainersPaginatedTests.cs index c0c001b1..05ca1bf0 100644 --- a/tests/Application.Tests.Integration/Rooms/Queries/GetEmptyContainersPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Queries/GetEmptyContainersPaginatedTests.cs @@ -1,11 +1,9 @@ -using Application.Lockers.Commands.AddLocker; -using Application.Rooms.Queries.GetEmptyContainersPaginated; +using Application.Rooms.Queries; using Bogus; using Domain.Entities.Physical; using FluentAssertions; using Infrastructure.Persistence; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualBasic; using Xunit; namespace Application.Tests.Integration.Rooms.Queries; @@ -20,8 +18,19 @@ public GetEmptyContainersPaginatedTests(CustomApiFactory apiFactory) : base(apiF public async Task ShouldReturnLockersWithEmptyFolders() { // Arrange - var room = await SetupTestEntities(); - var query = new GetEmptyContainersPaginatedQuery() + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + folder3.IsAvailable = false; + var locker1 = CreateLocker(folder1, folder2); + locker1.Capacity = 2; + var locker2 = CreateLocker(folder3); + locker2.Capacity = 2; + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetEmptyContainersPaginated.Query() { Page = 1, Size = 2, @@ -36,23 +45,24 @@ public async Task ShouldReturnLockersWithEmptyFolders() result.Items.First().Id.Should().Be(room.Lockers.ElementAt(0).Id); result.Items.First().Name.Should().Be(room.Lockers.ElementAt(0).Name); result.Items.First().Description.Should().Be(room.Lockers.ElementAt(0).Description); - result.Items.First().NumberOfFreeFolders.Should().Be(1); - result.Items.First().Capacity.Should().Be(4); - result.Items.First().NumberOfFolders.Should().Be(2); - result.Items.First().Folders.First().Id.Should().Be(room.Lockers.ElementAt(0).Folders.First().Id); - result.Items.First().Folders.First().Name.Should().Be(room.Lockers.ElementAt(0).Folders.First().Name); - result.Items.First().Folders.First().Description.Should().Be(room.Lockers.ElementAt(0).Folders.First().Description); - result.Items.First().Folders.First().Slot.Should().Be(3); + result.Items.First().NumberOfFreeFolders.Should().Be(2); + result.Items.First().Capacity.Should().Be(2); // Cleanup - await CleanupTestEntities(room); + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); } [Fact] public async Task ShouldThrowNotFound_WhenRoomDoesNotExist() { // Arrange - var query = new GetEmptyContainersPaginatedQuery() + var query = new GetEmptyContainersPaginated.Query() { Page = 1, Size = 2, @@ -65,92 +75,4 @@ public async Task ShouldThrowNotFound_WhenRoomDoesNotExist() // Assert await action.Should().ThrowAsync("Room does not exist"); } - - private async Task SetupTestEntities() - { - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.FirstName, - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 2, - }; - - var locker1 = new Locker() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.LastName, - Room = room, - Capacity = 4, - IsAvailable = true, - NumberOfFolders = 2 - }; - - var locker2 = new Locker() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.UserName, - Room = room, - Capacity = 4, - IsAvailable = false, - NumberOfFolders = 1 - }; - - var folder1 = new Folder() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.FirstName, - Locker = locker1, - IsAvailable = true, - Capacity = 3, - NumberOfDocuments = 0, - }; - - var folder2 = new Folder() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.LastName, - Locker = locker1, - IsAvailable = true, - Capacity = 2, - NumberOfDocuments = 3 - }; - - var folder3 = new Folder() - { - Id = Guid.NewGuid(), - Name = new Faker().Person.UserName, - Locker = locker2, - IsAvailable = false, - Capacity = 3, - NumberOfDocuments = 1 - }; - - using var scope = _scopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - await context.Rooms.AddAsync(room); - await context.Lockers.AddAsync(locker1); - await context.Lockers.AddAsync(locker2); - await context.Folders.AddAsync(folder1); - await context.Folders.AddAsync(folder2); - await context.Folders.AddAsync(folder3); - - await context.SaveChangesAsync(); - - return room; - } - - private async Task CleanupTestEntities(Room room) - { - using var scope = _scopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - context.RemoveRange(room.Lockers.SelectMany(l => l.Folders)); - context.RemoveRange(room.Lockers); - context.Remove(room); - - await context.SaveChangesAsync(); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs index 52a431c9..89842090 100644 --- a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs +++ b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs @@ -1,4 +1,4 @@ -using Application.Users.Commands.AddUser; +using Application.Users.Commands; using Bogus; using Domain.Entities; using FluentAssertions; @@ -8,14 +8,6 @@ namespace Application.Tests.Integration.Users.Commands; public class AddUserTests : BaseClassFixture { - private readonly Faker _userGenerator = new Faker() - .RuleFor(x => x.Username, faker => faker.Person.UserName) - .RuleFor(x => x.Email, faker => faker.Person.Email) - .RuleFor(x => x.FirstName, faker => faker.Person.FirstName) - .RuleFor(x => x.LastName, faker => faker.Person.LastName) - .RuleFor(x => x.Password, faker => faker.Random.String()) - .RuleFor(x => x.Role, faker => faker.Random.Word()) - .RuleFor(x => x.Position, faker => faker.Random.Word()); public AddUserTests(CustomApiFactory apiFactory) : base(apiFactory) { } @@ -24,29 +16,32 @@ public AddUserTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldCreateUser_WhenCreateDetailsAreValid() { // Arrange - var department = new Department() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.Department() - }; + var department = CreateDepartment(); await AddAsync(department); - var createUserCommand = _userGenerator.Generate(); - createUserCommand = createUserCommand with + + var command = new AddUser.Command() { - DepartmentId = department.Id + Username = new Faker().Person.UserName, + Email = new Faker().Person.Email, + FirstName = new Faker().Person.FirstName, + LastName = new Faker().Person.LastName, + Password = new Faker().Random.Word(), + Role = new Faker().Random.Word(), + DepartmentId = department.Id, + Position = new Faker().Random.Word(), }; // Act - var user = await SendAsync(createUserCommand); + var user = await SendAsync(command); // Assert - user.Username.Should().Be(createUserCommand.Username); - user.FirstName.Should().Be(createUserCommand.FirstName); - user.LastName.Should().Be(createUserCommand.LastName); + user.Username.Should().Be(command.Username); + user.FirstName.Should().Be(command.FirstName); + user.LastName.Should().Be(command.LastName); user.Department.Should().BeEquivalentTo(new { department.Id, department.Name }); - user.Email.Should().Be(createUserCommand.Email); - user.Role.Should().Be(createUserCommand.Role); - user.Position.Should().Be(createUserCommand.Position); + user.Email.Should().Be(command.Email); + user.Role.Should().Be(command.Role); + user.Position.Should().Be(command.Position); user.IsActive.Should().Be(true); user.IsActivated.Should().Be(false); diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 36b57359..28df4253 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -2,8 +2,6 @@ using Application.Common.Mappings; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; -using Application.Documents.Queries.GetAllDocumentsPaginated; -using Application.Rooms.Queries.GetEmptyContainersPaginated; using Application.Users.Queries; using AutoMapper; using Domain.Entities; From b90947314b02729e506b9e8ac88ece21eda49f52 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Mon, 29 May 2023 19:39:50 +0700 Subject: [PATCH 013/162] feat: Create codeql.yml (#110) Code quality --- .github/workflows/codeql.yml | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..cf9e4655 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "dev", main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "dev" ] + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 5f62bb5b471927c599f6550babf3748359a590a5 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 29 May 2023 21:05:05 +0700 Subject: [PATCH 014/162] Feat/get room by ID (#114) * add: integration test * feat: get room by id * refactor: remove unesscary validator and refactor name of handler --------- Co-authored-by: Nguyen Quang Chien --- src/Application/Rooms/Queries/GetRoomById.cs | 29 ++++++++++ .../Rooms/Queries/GetRoomByIdTests.cs | 55 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 6a3657bd..6e834b68 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -1,5 +1,9 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Rooms.Queries; @@ -9,4 +13,29 @@ public record Query : IRequest { public Guid RoomId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + return _mapper.Map(room); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs new file mode 100644 index 00000000..1f3ee018 --- /dev/null +++ b/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs @@ -0,0 +1,55 @@ +using Application.Rooms.Queries; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Rooms.Queries; + +public class GetRoomByIdTests : BaseClassFixture +{ + public GetRoomByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnRoom_WhenThatRoomExists() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + await AddAsync(room); + + var query = new GetRoomById.Query() + { + RoomId = room.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Id.Should().Be(room.Id); + result.Name.Should().Be(room.Name); + + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatRoomDoesNotExist() + { + // Arrange + var query = new GetRoomById.Query() + { + RoomId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Room does not exist."); + } +} \ No newline at end of file From 55d642e8f677a9f60099426f7746903c99d0cb7a Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 29 May 2023 21:10:54 +0700 Subject: [PATCH 015/162] Feat/enable room (#112) * add: integration test * feat: enable room * fix: resolve test * add: add validator for enable room * refactor: remove unesscary validator and refactor name of handler * refactor: grammar for a happy chien-san --------- Co-authored-by: Nguyen Quang Chien --- .../Common/Models/Dtos/Physical/RoomDto.cs | 2 +- src/Application/Rooms/Commands/EnableRoom.cs | 47 ++++++++++ .../Rooms/Commands/EnableRoomTests.cs | 94 +++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs diff --git a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs index a2ed7b95..ff0ab27a 100644 --- a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs @@ -11,7 +11,7 @@ public class RoomDto : IMapFrom public string Name { get; set; } = null!; public string? Description { get; set; } public StaffDto? Staff { get; set; } - public DepartmentDto? Department { get; set; } + public DepartmentDto Department { get; set; } public int Capacity { get; set; } public int NumberOfLockers { get; set; } public bool IsAvailable { get; set; } diff --git a/src/Application/Rooms/Commands/EnableRoom.cs b/src/Application/Rooms/Commands/EnableRoom.cs index 0e0cf048..af5d710b 100644 --- a/src/Application/Rooms/Commands/EnableRoom.cs +++ b/src/Application/Rooms/Commands/EnableRoom.cs @@ -1,12 +1,59 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Rooms.Commands; public class EnableRoom { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + RuleFor(x => x.RoomId) + .NotEmpty().WithMessage("RoomId is required."); + } + } public record Command : IRequest { public Guid RoomId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + if (room.IsAvailable) + { + throw new ConflictException("Room has already been enabled."); + } + + room.IsAvailable = true; + var result = _context.Rooms.Update(room); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs new file mode 100644 index 00000000..fa07d0b8 --- /dev/null +++ b/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs @@ -0,0 +1,94 @@ +using Application.Common.Exceptions; +using Application.Rooms.Commands; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Rooms.Commands; + +public class EnableRoomTests : BaseClassFixture +{ + public EnableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldEnableRoom_WhenRoomExistsAndIsDisabled() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + folder.IsAvailable = false; + locker.IsAvailable = false; + room.IsAvailable = false; + await AddAsync(room); + + var command = new EnableRoom.Command() + { + RoomId = room.Id + }; + + // Act + var result = await SendAsync(command); + + // Assert + var folderResult = await FindAsync(folder.Id); + var lockerResult = await FindAsync(locker.Id); + + result.IsAvailable.Should().BeTrue(); + folderResult!.IsAvailable.Should().BeFalse(); + lockerResult!.IsAvailable.Should().BeFalse(); + + // Cleanup + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() + { + // Arrange + var command = new EnableRoom.Command() + { + RoomId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Room does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRoomIsAlreadyAvailable() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + room.IsAvailable = true; + await AddAsync(room); + + var command = new EnableRoom.Command() + { + RoomId = room.Id + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Room has already been enabled."); + + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file From 1f127b81dea76085966e16d661823fb94ae8b243 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 31 May 2023 22:49:43 +0700 Subject: [PATCH 016/162] Feat/remove locker (#135) * add: integration test * fix: something * feat: implement remove locker fix: refactoring * refactoring * refactoring --------- Co-authored-by: Nguyen Quang Chien --- .../Common/Models/Dtos/DepartmentDto.cs | 11 ++- .../Common/Models/Dtos/Physical/RoomDto.cs | 11 ++- .../Lockers/Commands/DisableLocker.cs | 4 +- .../Lockers/Commands/RemoveLocker.cs | 57 ++++++++++++ src/Application/Rooms/Commands/AddRoom.cs | 2 + src/Application/Rooms/Commands/DisableRoom.cs | 4 +- src/Application/Rooms/Commands/RemoveRoom.cs | 4 +- .../Lockers/Commands/RemoveLockerTests.cs | 90 +++++++++++++++++++ 8 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs diff --git a/src/Application/Common/Models/Dtos/DepartmentDto.cs b/src/Application/Common/Models/Dtos/DepartmentDto.cs index 68d00db4..9ae2e4e6 100644 --- a/src/Application/Common/Models/Dtos/DepartmentDto.cs +++ b/src/Application/Common/Models/Dtos/DepartmentDto.cs @@ -1,5 +1,5 @@ using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; +using AutoMapper; using Domain.Entities; namespace Application.Common.Models.Dtos; @@ -8,5 +8,12 @@ public class DepartmentDto : IMapFrom { public Guid Id { get; set; } public string Name { get; set; } = null!; - public RoomDto? Room { get; set; } + public Guid? RoomId { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.RoomId, + opt => opt.MapFrom(src => src.Room!.Id)); + } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs index ff0ab27a..44fe55e0 100644 --- a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs @@ -10,9 +10,16 @@ public class RoomDto : IMapFrom public Guid Id { get; set; } public string Name { get; set; } = null!; public string? Description { get; set; } - public StaffDto? Staff { get; set; } - public DepartmentDto Department { get; set; } + public Guid? StaffId { get; set; } + public DepartmentDto? Department { get; set; } public int Capacity { get; set; } public int NumberOfLockers { get; set; } public bool IsAvailable { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.StaffId, + opt => opt.MapFrom(src => src.Staff!.Id)); + } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/DisableLocker.cs b/src/Application/Lockers/Commands/DisableLocker.cs index eaa4f40b..cbde208a 100644 --- a/src/Application/Lockers/Commands/DisableLocker.cs +++ b/src/Application/Lockers/Commands/DisableLocker.cs @@ -26,12 +26,12 @@ public record Command : IRequest public Guid LockerId { get; init; } } - public class RemoveLockerCommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public RemoveLockerCommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index 11c6472b..4a0ce1fc 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,12 +1,69 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Lockers.Commands; public class RemoveLocker { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.LockerId) + .NotEmpty().WithMessage("LockerId is required."); + } + } + public record Command : IRequest { public Guid LockerId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); + + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + var canNotRemove = await _context.Documents + .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) + > 0; + + if (canNotRemove) + { + throw new InvalidOperationException("Locker cannot be removed because it contains documents."); + } + + var room = locker.Room; + + var result = _context.Lockers.Remove(locker); + room.NumberOfLockers -= 1; + _context.Rooms.Update(room); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index a317d7cf..13449b93 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -82,6 +82,8 @@ public async Task Handle(Command request, CancellationToken cancellatio NumberOfLockers = 0, Capacity = request.Capacity, Department = department, + DepartmentId = request.DepartmentId, + IsAvailable = true, }; var result = await _context.Rooms.AddAsync(entity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Rooms/Commands/DisableRoom.cs b/src/Application/Rooms/Commands/DisableRoom.cs index 580b62c6..dd9c7afe 100644 --- a/src/Application/Rooms/Commands/DisableRoom.cs +++ b/src/Application/Rooms/Commands/DisableRoom.cs @@ -26,12 +26,12 @@ public record Command : IRequest public Guid RoomId { get; init; } } - public class DisableRoomCommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public DisableRoomCommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index ef079f47..9e35ddc1 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -25,12 +25,12 @@ public record Command : IRequest public Guid RoomId { get; init; } } - public class RemoveRoomCommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public RemoveRoomCommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; diff --git a/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs new file mode 100644 index 00000000..a5e19fcf --- /dev/null +++ b/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs @@ -0,0 +1,90 @@ +using Application.Lockers.Commands; +using Application.Rooms.Commands; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Lockers.Commands; + +public class RemoveLockerTests : BaseClassFixture +{ + public RemoveLockerTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldRemoveLocker_WhenLockerHasNoDocuments() + { + // Arrange + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); + await Add(room); + + var command = new RemoveLocker.Command() + { + LockerId = locker.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(locker.Id); + var removedLocker = await FindAsync(locker.Id); + removedLocker.Should().BeNull(); + + // Cleanup + Remove(await FindAsync(room.Id)); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRoomStillHasDocuments() + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(1); + var folder = CreateFolder(documents); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new RemoveLocker.Command() + { + LockerId = locker.Id, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Locker cannot be removed because it contains documents."); + + // Cleanup + Remove(documents.First()); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() + { + // Arrange + var command = new RemoveLocker.Command() + { + LockerId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Locker does not exist."); + } +} \ No newline at end of file From c0f6601ad29ba568313332e862486858f32bd077 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 31 May 2023 23:51:24 +0700 Subject: [PATCH 017/162] Feat/update locker (#136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: integration test * feat: implement update locker DOT IN TEST EXCEPTION MESSAGE, CHIEN * Update GetAllDepartments.cs (temporary workaround until chien provides a solution) * fuck chiến * Update UpdateLocker.cs * test * remove: exclude admin department --------- Co-authored-by: Nguyen Quang Chien --- .../Departments/Queries/GetAllDepartments.cs | 5 +- .../Lockers/Commands/UpdateLocker.cs | 68 +++++++++ .../Lockers/Commands/UpdateLockerTests.cs | 132 ++++++++++++++++++ 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs diff --git a/src/Application/Departments/Queries/GetAllDepartments.cs b/src/Application/Departments/Queries/GetAllDepartments.cs index a4e5d711..1a2f85c1 100644 --- a/src/Application/Departments/Queries/GetAllDepartments.cs +++ b/src/Application/Departments/Queries/GetAllDepartments.cs @@ -24,9 +24,10 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var departments = await _context.Departments.ToListAsync(cancellationToken); + var departments = await _context.Departments + .ToListAsync(cancellationToken); var result = new ReadOnlyCollection(_mapper.Map>(departments)); return result; } } -} \ No newline at end of file +} diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 580c1e6f..f389c468 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,10 +1,34 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Exceptions; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Lockers.Commands; public class UpdateLocker { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .NotEmpty() + .MaximumLength(64).WithMessage("Locker's name cannot exceed 64 characters."); + + RuleFor(x => x.Description) + .MaximumLength(256).WithMessage("Locker's description cannot exceed 256 characters."); + + RuleFor(x => x.Capacity) + .GreaterThan(0).WithMessage("Locker's capacity cannot be less than 1"); + } + } public record Command : IRequest { public Guid LockerId { get; init; } @@ -12,4 +36,48 @@ public record Command : IRequest public string? Description { get; init; } public int Capacity { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => x.Id.Equals(request.LockerId), cancellationToken); + + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + var duplicateLocker = await _context.Lockers.FirstOrDefaultAsync( + x => x.Name.Equals(request.Name), cancellationToken); + + if (duplicateLocker is not null && !duplicateLocker.Equals(locker)) + { + throw new ConflictException("New locker name already exists."); + } + + if (locker.NumberOfFolders > request.Capacity) + { + throw new ConflictException("New capacity cannot be less than current number of folders."); + } + + locker.Name = request.Name; + locker.Description = request.Description; + locker.Capacity = request.Capacity; + + var result = _context.Lockers.Update(locker); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs new file mode 100644 index 00000000..67031617 --- /dev/null +++ b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs @@ -0,0 +1,132 @@ +using Application.Common.Exceptions; +using Application.Lockers.Commands; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Lockers.Commands; + +public class UpdateLockerTests : BaseClassFixture +{ + public UpdateLockerTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUpdateLocker_WhenUpdateDetailsAreValid() + { + // Arrange + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new UpdateLocker.Command() + { + LockerId = locker.Id, + Name = "Something else", + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(locker.Id); + result.Name.Should().Be(command.Name); + result.Capacity.Should().Be(command.Capacity); + result.Description.Should().Be(command.Description); + + // Cleanup + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatLockerDoesNotExist() + { + // Arrange + var command = new UpdateLocker.Command() + { + LockerId = Guid.NewGuid(), + Name = "Something else", + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Locker does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewLockerNameHasAlreadyExistedInThatRoom() + { + // Arrange + var department = CreateDepartment(); + var duplicateNameLocker = CreateLocker(); + var locker = CreateLocker(); + var room = CreateRoom(department, duplicateNameLocker, locker); + await AddAsync(room); + + var command = new UpdateLocker.Command() + { + LockerId = locker.Id, + Name = duplicateNameLocker.Name, + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("New locker name already exists."); + + // Cleanup + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewCapacityIsLessThanCurrentNumberOfFolders() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var locker = CreateLocker(folder1, folder2); + locker.Capacity = 3; + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new UpdateLocker.Command() + { + LockerId = locker.Id, + Name = "Something else", + Capacity = 1, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("New capacity cannot be less than current number of folders."); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file From e07ed2663367d4bd8be218a60a48868d585f4466 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 1 Jun 2023 10:04:37 +0700 Subject: [PATCH 018/162] Feat/get all folders paginated (#134) * add: integration test and implementation * add: i forgot this * add: search --- src/Api/Controllers/FoldersController.cs | 1 + .../GetAllFoldersPaginatedQueryParameters.cs | 4 + .../Common/Extensions/StringExtensions.cs | 13 ++ .../Folders/Queries/GetAllFoldersPaginated.cs | 92 ++++++++++++ .../Queries/GetAllFoldersPaginatedTests.cs | 131 ++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 src/Application/Common/Extensions/StringExtensions.cs create mode 100644 tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 3365ec81..a08b36cc 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -47,6 +47,7 @@ public async Task>>> GetAllPaginate { RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index 1be3d84f..ae6399ab 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -14,6 +14,10 @@ public class GetAllFoldersPaginatedQueryParameters /// public Guid? LockerId { get; set; } /// + /// Search term + /// + public string? SearchTerm { get; init; } + /// /// Page number /// public int? Page { get; set; } diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs new file mode 100644 index 00000000..b2a723de --- /dev/null +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -0,0 +1,13 @@ +namespace Application.Common.Extensions; + +public static class StringExtensions +{ + public static bool MatchesPropertyName(this string input) + where T : class + { + var type = typeof(T); + var properties = type.GetProperties(); + + return properties.Any(property => string.Equals(property.Name, input)); + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 280a4c5c..c52fa7dd 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -1,18 +1,110 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Queries; public class GetAllFoldersPaginated { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.RoomId) + .Must((query, roomId) => roomId is not null || query.LockerId is null); + } + } + public record Query : IRequest> { public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } + public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var folders = _context.Folders.AsQueryable(); + var roomExists = request.RoomId is not null; + var lockerExists = request.LockerId is not null; + + if (lockerExists) + { + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.LockerId + && x.IsAvailable, cancellationToken); + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + if (locker.Room.Id != request.RoomId) + { + throw new ConflictException("Room does not match locker."); + } + + folders = folders.Where(x => x.Locker.Id == request.LockerId); + } + else if (roomExists) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId + && x.IsAvailable, cancellationToken); + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + folders = folders.Where(x => x.Locker.Room.Id == request.RoomId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + folders = folders.Where(x => + x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(LockerDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var result = await folders + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + + return result; + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs b/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs new file mode 100644 index 00000000..48377685 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs @@ -0,0 +1,131 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Folders.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Queries; + +public class GetAllFoldersPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllFoldersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnFolders() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(3); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder1, folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldReturnFoldersOfASpecificLocker() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query() + { + RoomId = room.Id, + LockerId = locker2.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldReturnNothing_WhenWrongPaginationDetailsAreProvided() + { + // Arrange + var department = CreateDepartment(); + var folder1 = CreateFolder(); + var folder2 = CreateFolder(); + var folder3 = CreateFolder(); + var locker1 = CreateLocker(folder1); + var locker2 = CreateLocker(folder2, folder3); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllFoldersPaginated.Query() + { + RoomId = room.Id, + LockerId = locker2.Id, + Page = -1, + Size = -4, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(folder1); + Remove(folder2); + Remove(folder3); + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } +} \ No newline at end of file From c53cf7736b867f8a4731b6d5166b4116a578402b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:43:51 +0700 Subject: [PATCH 019/162] fix/get all folders paginated (#139) * add: integration test and implementation * add: i forgot this * add: search * fix: search term untranslatable --- src/Application/Folders/Queries/GetAllFoldersPaginated.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index c52fa7dd..c34617ce 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -87,7 +87,7 @@ public async Task> Handle(Query request, CancellationTo if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { folders = folders.Where(x => - x.Name.Contains(request.SearchTerm, StringComparison.InvariantCultureIgnoreCase)); + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } var sortBy = request.SortBy; From 94402aab4fd2a2667140a16554f5e5f7b9a0d0ea Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:06:38 +0700 Subject: [PATCH 020/162] Feat/get folder by Id (#140) * add: integration test * feat(GetFolderById.cs): implementation for get folder by id --------- Co-authored-by: Nguyen Quang Chien --- .../Folders/Queries/GetFolderById.cs | 27 +++++++++ .../Folders/Queries/GetFolderByIdTests.cs | 59 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs index fd0d0b57..9b1d07df 100644 --- a/src/Application/Folders/Queries/GetFolderById.cs +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -1,5 +1,8 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Queries; @@ -9,4 +12,28 @@ public record Query : IRequest { public Guid FolderId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var folder = await _context.Folders.FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); + + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + return _mapper.Map(folder); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs b/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs new file mode 100644 index 00000000..d4d078b6 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs @@ -0,0 +1,59 @@ +using Application.Folders.Queries; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Queries; + +public class GetFolderByIdTests : BaseClassFixture +{ + public GetFolderByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnFolder_WhenThatFolderExists() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var query = new GetFolderById.Query() + { + FolderId = folder.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Id.Should().Be(folder.Id); + result.Name.Should().Be(folder.Name); + + // Cleanup + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() + { + // Arrange + var query = new GetFolderById.Query() + { + FolderId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Folder does not exist."); + } +} \ No newline at end of file From 22aebca0c6435a1022783172da1ea7be122fac1c Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:11:21 +0700 Subject: [PATCH 021/162] Feat/get user by Id (#117) * add: integration test * feat: get user by id * refactor: added a dot for convention --------- Co-authored-by: Nguyen Quang Chien --- src/Application/Users/Queries/GetUserById.cs | 28 +++++++++ .../Users/Queries/GetUserByIdTests.cs | 59 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs index e34b58d2..c4cd1278 100644 --- a/src/Application/Users/Queries/GetUserById.cs +++ b/src/Application/Users/Queries/GetUserById.cs @@ -1,4 +1,7 @@ +using Application.Common.Interfaces; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Users.Queries; @@ -8,4 +11,29 @@ public record Query : IRequest { public Guid UserId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var user = await _context.Users + .FirstOrDefaultAsync(x => x.Id.Equals(request.UserId), cancellationToken: cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + return _mapper.Map(user); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs b/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs new file mode 100644 index 00000000..f6a758be --- /dev/null +++ b/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs @@ -0,0 +1,59 @@ +using Application.Identity; +using Application.Users.Queries; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Users.Queries; + +public class GetUserByIdTests : BaseClassFixture +{ + public GetUserByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnUser_WhenThatUserExists() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "randomPassword"); + await AddAsync(user); + + var query = new GetUserById.Query() + { + UserId = user.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Username.Should().Be(user.Username); + result.Email.Should().Be(user.Email); + result.FirstName.Should().Be(user.FirstName); + result.LastName.Should().Be(user.LastName); + result.Role.Should().Be(user.Role); + result.Position.Should().Be(user.Position); + result.IsActivated.Should().Be(user.IsActivated); + result.IsActive.Should().Be(user.IsActive); + + // Cleanup + Remove(user); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatUserDoesNotExist() + { + // Arrange + var query = new GetUserById.Query() + { + UserId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("User does not exist."); + } +} \ No newline at end of file From 7dc19a12c669e11daebc9b6d813090fcaf72dc99 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:21:12 +0700 Subject: [PATCH 022/162] Feat/update user (#120) * add: integration test * feat: update user * refactor: add validator --------- Co-authored-by: Nguyen Quang Chien --- .../Requests/Users/UpdateUserRequest.cs | 4 -- src/Api/Controllers/UsersController.cs | 1 - src/Application/Users/Commands/UpdateUser.cs | 52 +++++++++++++++- .../Users/Commands/UpdateUserTests.cs | 60 +++++++++++++++++++ 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs index d162c28a..c30e9871 100644 --- a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs @@ -14,10 +14,6 @@ public class UpdateUserRequest /// public string? LastName { get; set; } /// - /// New role of the user to be updated - /// - public string Role { get; set; } = null!; - /// /// New position of the user to be updated /// public string? Position { get; set; } diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index dc2b406e..cc0c4ccc 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -141,7 +141,6 @@ public async Task>> Update([FromRoute] Guid userId, UserId = userId, FirstName = request.FirstName, LastName = request.LastName, - Role = request.Role, Position = request.Position, }; var result = await Mediator.Send(command); diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 4d894cc0..00b27531 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,16 +1,66 @@ +using Application.Common.Interfaces; using Application.Users.Queries; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Users.Commands; public class UpdateUser { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.FirstName) + .MaximumLength(50).WithMessage("FirstName can not exceed 50 characters."); + + RuleFor(x => x.LastName) + .MaximumLength(50).WithMessage("LastName can not exceed 50 characters."); + + RuleFor(x => x.Position) + .MaximumLength(64).WithMessage("Position can not exceed 64 characters."); + } + } public record Command : IRequest { public Guid UserId { get; init; } public string? FirstName { get; init; } public string? LastName { get; init; } - public string Role { get; init; } = null!; public string? Position { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users + .FirstOrDefaultAsync(x => x.Id.Equals(request.UserId), cancellationToken: cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + user.FirstName = request.FirstName; + user.LastName = request.LastName; + user.Position = request.Position; + + var result = _context.Users.Update(user); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs new file mode 100644 index 00000000..3214a6bd --- /dev/null +++ b/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs @@ -0,0 +1,60 @@ +using Application.Identity; +using Application.Users.Commands; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Users.Commands; + +public class UpdateUserTests : BaseClassFixture +{ + public UpdateUserTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUpdateUser_WhenUpdateDetailsAreValid() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "randompassword"); + await AddAsync(user); + + var command = new UpdateUser.Command() + { + UserId = user.Id, + FirstName = "khoa", + LastName = "ngu", + Position = "IDK", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.FirstName.Should().Be(command.FirstName); + result.LastName.Should().Be(command.LastName); + result.Position.Should().Be(command.Position); + + // Cleanup + Remove(user); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatUserDoesNotExist() + { + // Arrange + var command = new UpdateUser.Command() + { + UserId = Guid.NewGuid(), + FirstName = "khoa", + LastName = "ngu", + Position = "IDK", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("User does not exist."); + } +} \ No newline at end of file From dfe1a8516a263a4483b24f16f0b59f251e8f217a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 1 Jun 2023 19:36:50 +0700 Subject: [PATCH 023/162] feat: get all rooms paginated (#132) * add: integration test and implementation * fix: my life * fix: my life again * add: search * forgive: me * fix: search term untranslatable --- .../GetAllLockersPaginatedQueryParameters.cs | 4 + .../GetAllRoomsPaginatedQueryParameters.cs | 4 + src/Api/Controllers/RoomsController.cs | 4 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 45 ++++++++++ .../Queries/GetAllRoomsPaginatedTests.cs | 84 +++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index 656b96f6..9f701ed4 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -12,6 +12,10 @@ public class GetAllLockersPaginatedQueryParameters /// public Guid? RoomId { get; set; } /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// /// Page number /// public int? Page { get; set; } diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 34721c39..89a7bbb6 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -5,6 +5,10 @@ namespace Api.Controllers.Payload.Requests.Rooms; /// public class GetAllRoomsPaginatedQueryParameters { + /// + /// Search term + /// + public string? SearchTerm { get; set; } /// /// Page number /// diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index d46390cc..5ecb848d 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -36,14 +36,16 @@ public async Task>> GetById([FromRoute] Guid roomId /// /// Get all rooms paginated details /// A paginated list of rooms + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( - [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) + [FromQuery] GetAllRoomsPaginatedQueryParameters queryParameters) { var query = new GetAllRoomsPaginated.Query() { + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 8094f631..aef45ae6 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -1,5 +1,10 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; using MediatR; namespace Application.Rooms.Queries; @@ -8,9 +13,49 @@ public class GetAllRoomsPaginated { public record Query : IRequest> { + public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var rooms = _context.Rooms.AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + rooms = rooms.Where(x => + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(RoomDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var result = await rooms + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + + return result; + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs new file mode 100644 index 00000000..83c6c955 --- /dev/null +++ b/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs @@ -0,0 +1,84 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Rooms.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Rooms.Queries; + +public class GetAllRoomsPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllRoomsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnAllRooms() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var room1 = CreateRoom(department1); + var room2 = CreateRoom(department2); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllRoomsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .ContainEquivalentOf(_mapper.Map(room1), + config => config.IgnoringCyclicReferences()); + result.Items.Should() + .ContainEquivalentOf(_mapper.Map(room2), + config => config.IgnoringCyclicReferences()); + + // Cleanup + Remove(room1); + Remove(room2); + Remove(department1); + Remove(department2); + } + + [Fact] + public async Task ShouldReturnOrderById_WhenWrongSortByIsProvided() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var room1 = CreateRoom(department1); + var room2 = CreateRoom(department2); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllRoomsPaginated.Query() + { + SortBy = "e", + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { room1, room2 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(room1); + Remove(room2); + Remove(department1); + Remove(department2); + } +} \ No newline at end of file From 04de93684d6013b5b55e5bfae2f5bdc98d3e556a Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:00:00 +0700 Subject: [PATCH 024/162] Feat/update room (#115) * add: integration test * update: error message in test * feat: update a room * refactor: add validator and fix grammar * fix: my soul * refactor: validator refactor to fit with project convention --------- Co-authored-by: Nguyen Quang Chien --- src/Application/Rooms/Commands/UpdateRoom.cs | 84 +++++++++++- .../Rooms/Commands/UpdateRoomTests.cs | 128 ++++++++++++++++++ 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index 3dc20417..ade660fe 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,15 +1,97 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Rooms.Commands; public class UpdateRoom { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name can not be empty.") + .MaximumLength(64).WithMessage("Name can not exceed 64 characters."); + + RuleFor(x => x.Capacity) + .NotEmpty().WithMessage("Capacity can not be empty."); + + RuleFor(x => x.Description) + .MaximumLength(256).WithMessage("Description can not exceed 256 characters."); + } + } public record Command : IRequest { public Guid RoomId { get; init; } - public string Name { get; set; } = null!; + public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var nameExisted = await _context.Rooms.AnyAsync(x => x.Name + .ToLower().Equals(request.Name.ToLower()) + && x.Id != room.Id + , cancellationToken: cancellationToken); + + if (nameExisted) + { + throw new ConflictException("Name has already exists."); + } + + if (request.Capacity < room.NumberOfLockers) + { + throw new ConflictException("New capacity cannot be less than current number of lockers."); + } + + var updatedRoom = new Room + { + Id = room.Id, + Name = request.Name, + Description = request.Description, + Staff = room.Staff, + Department = room.Department, + DepartmentId = room.DepartmentId, + Capacity = request.Capacity, + NumberOfLockers = room.NumberOfLockers, + IsAvailable = room.IsAvailable, + Lockers = room.Lockers + }; + + _context.Rooms.Entry(room).State = EntityState.Detached; + _context.Rooms.Entry(updatedRoom).State = EntityState.Modified; + + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(updatedRoom); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs new file mode 100644 index 00000000..25f0b5b1 --- /dev/null +++ b/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs @@ -0,0 +1,128 @@ +using Application.Common.Exceptions; +using Application.Rooms.Commands; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Rooms.Commands; + +public class UpdateRoomTests : BaseClassFixture +{ + public UpdateRoomTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUpdateRoom_WhenUpdateDetailsAreValid() + { + // Act + var department = CreateDepartment(); + var room = CreateRoom(department); + await AddAsync(room); + + var command = new UpdateRoom.Command() + { + RoomId = room.Id, + Name = "Something else", + Description = "Description else", + Capacity = 6, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Name.Should().Be(command.Name); + result.Description.Should().Be(command.Description); + result.Capacity.Should().Be(command.Capacity); + + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() + { + // Act + var command = new UpdateRoom.Command() + { + RoomId = Guid.NewGuid(), + Name = "Something else", + Description = "Description else", + Capacity = 6, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Room does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewCapacityIsLowerThanNumberOfCurrentLockers() + { + // Act + var department = CreateDepartment(); + var locker1 = CreateLocker(); + var locker2 = CreateLocker(); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var command = new UpdateRoom.Command() + { + RoomId = room.Id, + Name = "Something else", + Description = "Description else", + Capacity = 1, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("New capacity cannot be less than current number of lockers."); + + // Cleanup + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewNameHasAlreadyExisted() + { + // Act + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var existedNameRoom = CreateRoom(department1); + var room = CreateRoom(department2); + await AddAsync(existedNameRoom); + await AddAsync(room); + + var command = new UpdateRoom.Command() + { + RoomId = room.Id, + Name = existedNameRoom.Name, + Description = "Description else", + Capacity = 1, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Name has already exists."); + + // Cleanup + Remove(existedNameRoom); + Remove(room); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); + } +} \ No newline at end of file From 4e2864d443b7da8fb1ad9bb0b508795fd047d59b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:03:55 +0700 Subject: [PATCH 025/162] feat: get all lockers integration tests and implementation (#133) * add: integration test and implementation * fix: my life again * add: search * fix: search term untranslatable --- src/Api/Controllers/LockersController.cs | 1 + .../Lockers/Queries/GetAllLockersPaginated.cs | 50 +++++++++++ .../Queries/GetAllLockersPaginatedTests.cs | 89 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 373a0cee..2445df95 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -44,6 +44,7 @@ public async Task>>> GetAllPaginate var query = new GetAllLockersPaginated.Query() { RoomId = queryParameters.RoomId, + SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index f7935335..794764b5 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -1,5 +1,10 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; using MediatR; namespace Application.Lockers.Queries; @@ -9,9 +14,54 @@ public class GetAllLockersPaginated public record Query : IRequest> { public Guid? RoomId { get; init; } + public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var lockers = _context.Lockers.AsQueryable(); + + if (request.RoomId is not null) + { + lockers = lockers.Where(x => x.Room.Id == request.RoomId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + lockers = lockers.Where(x => + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(LockerDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var result = await lockers + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByCustom(sortBy, sortOrder) + .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + + return result; + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs b/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs new file mode 100644 index 00000000..6693ea51 --- /dev/null +++ b/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs @@ -0,0 +1,89 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Lockers.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Lockers.Queries; + +public class GetAllLockersPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllLockersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnLockers() + { + // Arrange + var department = CreateDepartment(); + var locker1 = CreateLocker(); + var locker2 = CreateLocker(); + var room = CreateRoom(department, locker1, locker2); + await AddAsync(room); + + var query = new GetAllLockersPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { locker1, locker2 }) + .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); + + // Cleanup + Remove(locker1); + Remove(locker2); + Remove(room); + Remove(department); + } + + [Fact] + public async Task ShouldReturnLockersOfOneRoom_WhenSpecifyIdOfThatRoom() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var locker1 = CreateLocker(); + var locker2 = CreateLocker(); + var room1 = CreateRoom(department1, locker1, locker2); + var locker3 = CreateLocker(); + var locker4 = CreateLocker(); + var room2 = CreateRoom(department2, locker3, locker4); + await AddAsync(room1); + await AddAsync(room2); + + var query = new GetAllLockersPaginated.Query() + { + RoomId = room1.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should().ContainEquivalentOf(_mapper.Map(locker1), + config => config.IgnoringCyclicReferences()); + result.Items.Should().ContainEquivalentOf(_mapper.Map(locker2), + config => config.IgnoringCyclicReferences()); + result.Items.Should().NotContainEquivalentOf(_mapper.Map(locker3), + config => config.IgnoringCyclicReferences()); + result.Items.Should().NotContainEquivalentOf(_mapper.Map(locker4), + config => config.IgnoringCyclicReferences()); + + // Cleanup + Remove(locker1); + Remove(locker2); + Remove(room1); + Remove(room2); + Remove(department1); + Remove(department2); + } +} \ No newline at end of file From e99b63cf2d11a9d303c6196f53d73be22e333a4e Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:28:35 +0700 Subject: [PATCH 026/162] Feat/remove staff (#142) * add: Remove staff * test: add test for remove staff * test: add test * add: documentation for endpoint --- src/Api/Controllers/StaffsController.cs | 21 +++++++ .../Staffs/Commands/RemoveStaff.cs | 45 ++++++++++++++ .../Staffs/Commands/RemoveStaffTests.cs | 60 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/Application/Staffs/Commands/RemoveStaff.cs create mode 100644 tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 048f1b4c..7fdad81b 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -112,4 +112,25 @@ public async Task>> RemoveFromRoom( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Remove a staff + /// + /// Id of the staff to be removed + /// A StaffDto of the removed staff + [HttpDelete("{staffId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Remove( + [FromRoute] Guid staffId) + { + var command = new RemoveStaff.Command() + { + StaffId = staffId + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaff.cs b/src/Application/Staffs/Commands/RemoveStaff.cs new file mode 100644 index 00000000..d550094b --- /dev/null +++ b/src/Application/Staffs/Commands/RemoveStaff.cs @@ -0,0 +1,45 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Staffs.Commands; + +public class RemoveStaff +{ + public record Command : IRequest + { + public Guid StaffId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.User.Id.Equals(request.StaffId), cancellationToken: cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + var result = _context.Staffs.Remove(staff); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs new file mode 100644 index 00000000..92df91b8 --- /dev/null +++ b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs @@ -0,0 +1,60 @@ +using Application.Identity; +using Application.Staffs.Commands; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Staffs.Commands; + +public class RemoveStaffTests : BaseClassFixture +{ + public RemoveStaffTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldRemoveStaff_WhenStaffIdIsValid() + { + // Arrange + var department = CreateDepartment(); + var user = CreateUser(IdentityData.Roles.Admin, "123456"); + var room = CreateRoom(department); + var staff = CreateStaff(user, room); + await AddAsync(staff); + + var command = new RemoveStaff.Command() + { + StaffId = staff.Id + }; + + // Act + await SendAsync(command); + + // Assert + var result = await FindAsync(staff.Id); + result.Should().BeNull(); + + // Cleanup + Remove(await FindAsync(room.Id)); + Remove(user); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() + { + // Arrange + var command = new RemoveStaff.Command() + { + StaffId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Staff does not exist."); + } +} \ No newline at end of file From ab96896f4817abc83a90ef69e73f70564ae9c8bf Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:34:18 +0700 Subject: [PATCH 027/162] Feat/get department by id (#154) * add: integration test * feat: implement get department by id --------- Co-authored-by: Nguyen Quang Chien --- .../Departments/Queries/GetDepartmentById.cs | 28 ++++++++++ .../Queries/GetDepartmentByIdTests.cs | 52 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs diff --git a/src/Application/Departments/Queries/GetDepartmentById.cs b/src/Application/Departments/Queries/GetDepartmentById.cs index e4120c64..83f3f725 100644 --- a/src/Application/Departments/Queries/GetDepartmentById.cs +++ b/src/Application/Departments/Queries/GetDepartmentById.cs @@ -1,5 +1,9 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Departments.Queries; @@ -9,4 +13,28 @@ public record Query : IRequest { public Guid DepartmentId { get; init; } } + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var department = await _context.Departments.FirstOrDefaultAsync(x => x.Id.Equals(request.DepartmentId), cancellationToken); + + if (department is null) + { + throw new KeyNotFoundException("Department does not exist."); + } + + return _mapper.Map(department); + } + } + } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs b/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs new file mode 100644 index 00000000..af7d9df8 --- /dev/null +++ b/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs @@ -0,0 +1,52 @@ +using Application.Departments.Queries; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Departments.Queries; + +public class GetDepartmentByIdTests : BaseClassFixture +{ + public GetDepartmentByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnDepartment_WhenThatDepartmentExists() + { + // Arrange + var department = CreateDepartment(); + + await AddAsync(department); + + var query = new GetDepartmentById.Query() + { + DepartmentId = department.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Name.Should().Be(department.Name); + + // Cleanup + Remove(department); + } + + [Fact] + public async Task ShouldThrowNotFoundException_WhenThatDepartmentDoesNotExist() + { + // Arrange + var query = new GetDepartmentById.Query() + { + DepartmentId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Department does not exist."); + } +} \ No newline at end of file From e65e727bd7520b2a3f2925f483437a11e9cd86ba Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:35:17 +0700 Subject: [PATCH 028/162] Feat/get locker by id (#155) * add: integration test * feat: implement get locker my id --------- Co-authored-by: Nguyen Quang Chien --- .../Lockers/Queries/GetLockerById.cs | 27 +++++++++ .../Lockers/Queries/GetLockerByIdTests.cs | 57 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 65c31416..8077335f 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -1,5 +1,8 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Lockers.Queries; @@ -9,4 +12,28 @@ public record Query : IRequest { public Guid LockerId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); + + if (locker is null) + { + throw new KeyNotFoundException("Locker does not exist."); + } + + return _mapper.Map(locker); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs b/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs new file mode 100644 index 00000000..be791603 --- /dev/null +++ b/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs @@ -0,0 +1,57 @@ +using Application.Lockers.Queries; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Lockers.Queries; + +public class GetLockerByIdTests : BaseClassFixture +{ + public GetLockerByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnLocker_WhenThatLockerExists() + { + // Arrange + var department = CreateDepartment(); + var locker = CreateLocker(); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var query = new GetLockerById.Query() + { + LockerId = locker.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Id.Should().Be(locker.Id); + result.Name.Should().Be(locker.Name); + + // Cleanup + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatLockerDoesNotExist() + { + // Arrange + var query = new GetLockerById.Query() + { + LockerId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Locker does not exist."); + } +} \ No newline at end of file From 8d541052bb98bd543be0f53591a2f2e0c82b4118 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Thu, 1 Jun 2023 22:43:39 +0700 Subject: [PATCH 029/162] Feat/update folder (#158) * add: integration test * update folder feature and fix tests --------- Co-authored-by: Nguyen Quang Chien --- .../Folders/Commands/UpdateFolder.cs | 70 +++++++++ .../Folders/Commands/UpdateFolderTests.cs | 138 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index a467f7e3..56fec760 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,10 +1,34 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Commands; public class UpdateFolder { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(f => f.Name) + .NotEmpty().WithMessage("Name is required.") + .MaximumLength(64).WithMessage("Name cannot exceed 64 characters."); + + RuleFor(f => f.Description) + .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); + + RuleFor(f => f.Capacity) + .NotEmpty().WithMessage("Folder capacity is required.") + .GreaterThanOrEqualTo(1).WithMessage("Folder's capacity cannot be less than 1."); + } + } + public record Command : IRequest { public Guid FolderId { get; init; } @@ -12,4 +36,50 @@ public record Command : IRequest public string? Description { get; init; } public int Capacity { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); + + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + var nameExisted = await _context.Folders.AnyAsync( x => + x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) + && x.Id != folder.Id + , cancellationToken); + + if (nameExisted) + { + throw new ConflictException("Folder name already exists."); + } + + if (request.Capacity < folder.NumberOfDocuments) + { + throw new ConflictException("New capacity cannot be less than current number of documents."); + } + + folder.Name = request.Name; + folder.Description = request.Description; + folder.Capacity = request.Capacity; + + var result = _context.Folders.Update(folder); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs new file mode 100644 index 00000000..c3df9928 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs @@ -0,0 +1,138 @@ +using Application.Common.Exceptions; +using Application.Folders.Commands; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Commands; + +public class UpdateFolderTests : BaseClassFixture +{ + public UpdateFolderTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUpdateFolder_WhenUpdateDetailsAreValid() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new UpdateFolder.Command() + { + FolderId = folder.Id, + Name = "Something else", + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(folder.Id); + result.Name.Should().Be(command.Name); + result.Capacity.Should().Be(command.Capacity); + result.Description.Should().Be(command.Description); + + // Cleanup + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() + { + // Arrange + var command = new UpdateFolder.Command() + { + FolderId = Guid.NewGuid(), + Name = "Something else", + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Folder does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewFolderNameHasAlreadyExistedInThatLocker() + { + // Arrange + var department = CreateDepartment(); + var duplicateNameFolder = CreateFolder(); + var folder = CreateFolder(); + var locker = CreateLocker(duplicateNameFolder, folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new UpdateFolder.Command() + { + FolderId = folder.Id, + Name = duplicateNameFolder.Name, + Capacity = 6, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Folder name already exists."); + + // Cleanup + Remove(folder); + Remove(duplicateNameFolder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewCapacityIsLessThanCurrentNumberOfDocuments() + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(2); + var folder = CreateFolder(documents); + folder.Capacity = 3; + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new UpdateFolder.Command() + { + FolderId = folder.Id, + Name = "Something else", + Capacity = 1, + Description = "ehehe", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("New capacity cannot be less than current number of documents."); + + // Cleanup + Remove(documents[0]); + Remove(documents[1]); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file From f0f6183ff6e10b1b11349345c65797b277c9affd Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Fri, 2 Jun 2023 00:12:57 +0700 Subject: [PATCH 030/162] Feat/remove folder (#141) * add: integration test * feat(RemoveFolder.cs): add remove folder feature * Fix(RemoveFolderTest.cs): Sorry for not doing review code properly. Lesson learnt: never trust codes from a sleepy coder! * fix: number of folder in locker decrease by 1 --------- Co-authored-by: Nguyen Quang Chien --- .../Folders/Commands/RemoveFolder.cs | 44 +++++++++ .../Folders/Commands/RemoveFolderTests.cs | 94 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index 528184ce..30fb5776 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,5 +1,11 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Commands; @@ -9,4 +15,42 @@ public record Command : IRequest { public Guid FolderId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var folder = await _context.Folders + .Include(x => x.Locker) + .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); + + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + var containDocument = folder.NumberOfDocuments > 0; + + if (containDocument) + { + throw new ConflictException("Folder cannot be removed because it contains documents."); + } + + var locker = folder.Locker; + var result = _context.Folders.Remove(folder); + locker.NumberOfFolders -= 1; + + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs new file mode 100644 index 00000000..a03af6b3 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs @@ -0,0 +1,94 @@ +using Application.Common.Exceptions; +using Application.Folders.Commands; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Commands; + +public class RemoveFolderTests : BaseClassFixture +{ + public RemoveFolderTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldRemoveFolder_WhenFolderHasNoDocuments() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await Add(room); + + var command = new RemoveFolder.Command() + { + FolderId = folder.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(folder.Id); + var removedFolder = await FindAsync(folder.Id); + removedFolder.Should().BeNull(); + var lockerOfRemovedFolder = await FindAsync(locker.Id); + locker.NumberOfFolders.Should().Be(lockerOfRemovedFolder!.NumberOfFolders + 1); + + // Cleanup + Remove(lockerOfRemovedFolder); + Remove(await FindAsync(room.Id)); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenFolderStillHasDocuments() + { + // Arrange + var department = CreateDepartment(); + var documents = CreateNDocuments(1); + var folder = CreateFolder(documents); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new RemoveFolder.Command() + { + FolderId = folder.Id + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Folder cannot be removed because it contains documents."); + + // Cleanup + Remove(documents.First()); + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenFolderDoesNotExist() + { + // Arrange + var command = new RemoveFolder.Command() + { + FolderId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Folder does not exist."); + } +} \ No newline at end of file From 50801d80de407960e9a355dc0f7f7f1a3d269e8c Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:30:21 +0700 Subject: [PATCH 031/162] fix: paginations (#161) --- .../Common/Extensions/QueryableExtensions.cs | 5 ++++ .../Queries/GetAllDocumentsPaginated.cs | 28 +++++++++++++------ .../Folders/Queries/GetAllFoldersPaginated.cs | 17 +++++++---- .../Lockers/Queries/GetAllLockersPaginated.cs | 16 +++++++---- .../Rooms/Queries/GetAllRoomsPaginated.cs | 17 +++++++---- .../Queries/GetAllDocumentsPaginatedTests.cs | 3 +- 6 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 0c5c5f2a..2272a400 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -20,4 +20,9 @@ public static IQueryable OrderByCustom(this IQueryable(result); } + + public static IQueryable Paginate(this IQueryable items, int page, int size) + { + return items.Skip((page - 1) * page).Take(size); + } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index c6c34a92..e3695cb9 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -69,7 +69,12 @@ public async Task> Handle(Query request, var lockerExists = request.LockerId is not null; var folderExists = request.FolderId is not null; - documents = documents.Include(x => x.Department); + documents = documents + .Include(x => x.Department) + .Include(x => x.Folder) + .ThenInclude(y => y.Locker) + .ThenInclude(z => z.Room) + .ThenInclude(t => t.Department); if (folderExists) { @@ -123,16 +128,23 @@ public async Task> Handle(Query request, documents = documents.Where(x => x.Folder!.Locker.Room.Id == request.RoomId); } - var sortBy = request.SortBy ?? nameof(DocumentDto.Id); + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(LockerDto.Id); + } var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page ?? 1; - var sizeNumber = request.Size ?? 5; - var result = await documents - .ProjectTo(_mapper.ConfigurationProvider) + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await documents + .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber, sizeNumber); + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); - return result; + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index c34617ce..9a4b149f 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -6,6 +6,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -49,7 +50,11 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var folders = _context.Folders.AsQueryable(); + var folders = _context.Folders + .Include(x => x.Locker) + .ThenInclude(y => y.Room) + .ThenInclude(z => z.Department) + .AsQueryable(); var roomExists = request.RoomId is not null; var lockerExists = request.LockerId is not null; @@ -99,12 +104,14 @@ public async Task> Handle(Query request, CancellationTo var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - var result = await folders - .ProjectTo(_mapper.ConfigurationProvider) + var list = await folders + .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); - return result; + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 794764b5..8485fffe 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -6,6 +6,7 @@ using AutoMapper; using AutoMapper.QueryableExtensions; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Lockers.Queries; @@ -34,7 +35,10 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var lockers = _context.Lockers.AsQueryable(); + var lockers = _context.Lockers + .Include(x => x.Room) + .ThenInclude(y => y.Department) + .AsQueryable(); if (request.RoomId is not null) { @@ -56,12 +60,14 @@ public async Task> Handle(Query request, CancellationTo var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - var result = await lockers - .ProjectTo(_mapper.ConfigurationProvider) + var list = await lockers + .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); - return result; + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index aef45ae6..e261e6b7 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -6,6 +6,7 @@ using AutoMapper; using AutoMapper.QueryableExtensions; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Rooms.Queries; @@ -33,7 +34,9 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var rooms = _context.Rooms.AsQueryable(); + var rooms = _context.Rooms + .Include(x => x.Department) + .AsQueryable(); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { @@ -49,13 +52,15 @@ public async Task> Handle(Query request, CancellationToke var sortOrder = request.SortOrder ?? "asc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var result = await rooms - .ProjectTo(_mapper.ConfigurationProvider) + + var list = await rooms + .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) - .PaginatedListAsync(pageNumber.Value, sizeNumber.Value); + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); - return result; + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs index 42d3b5d1..3863d1f4 100644 --- a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs @@ -378,7 +378,8 @@ public async Task ShouldReturnSortedByIdPaginatedList_WhenSortByIsNotPresent() // Assert result.Items.First().Should() .BeEquivalentTo( - _mapper.Map(documents[0].Id.CompareTo(documents[1].Id) <= 0 ? documents[0] : documents[1]), + _mapper.Map(documents) + .OrderBy(x => x.Id).First(), x => x.IgnoringCyclicReferences()); // Cleanup From 40857481f9ae957faa78ae3f97465ad3880d2a95 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:35:46 +0700 Subject: [PATCH 032/162] Feat/enable folder (#144) * add: integration test * implementation for enable folder --------- Co-authored-by: Nguyen Quang Chien --- .../Folders/Commands/EnableFolder.cs | 37 ++++++++ .../Folders/Commands/EnableFolderTests.cs | 90 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs diff --git a/src/Application/Folders/Commands/EnableFolder.cs b/src/Application/Folders/Commands/EnableFolder.cs index 33e3cb23..2cd66702 100644 --- a/src/Application/Folders/Commands/EnableFolder.cs +++ b/src/Application/Folders/Commands/EnableFolder.cs @@ -1,5 +1,9 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Folders.Commands; @@ -9,4 +13,37 @@ public record Command : IRequest { public Guid FolderId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); + + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + if (folder.IsAvailable) + { + throw new ConflictException("Folder has already been enabled."); + } + + folder.IsAvailable = true; + var result = _context.Folders.Update(folder); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs new file mode 100644 index 00000000..62f0d165 --- /dev/null +++ b/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs @@ -0,0 +1,90 @@ +using Application.Common.Exceptions; +using Application.Folders.Commands; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Folders.Commands; + +public class EnableFolderTests : BaseClassFixture +{ + public EnableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldEnableFolder_WhenThatFolderExistsAndIsDisabled() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + folder.IsAvailable = false; + await AddAsync(room); + + var command = new EnableFolder.Command() + { + FolderId = folder.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.IsAvailable.Should().BeTrue(); + + // Cleanup + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() + { + // Arrange + var command = new EnableFolder.Command() + { + FolderId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Folder does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenFolderIsAlreadyAvailable() + { + // Arrange + var department = CreateDepartment(); + var folder = CreateFolder(); + folder.IsAvailable = true; + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); + + var command = new EnableFolder.Command() + { + FolderId = folder.Id, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Folder has already been enabled."); + + // Cleanup + Remove(folder); + Remove(locker); + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file From a7e919389812c708faf0413981c31fc60379bdec Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:44:14 +0700 Subject: [PATCH 033/162] feat: remove user from room + tests (#156) * feat: remove user from room + tests * add: clearance for handler * fix: test resolve * fix: i am a stoopid person, test resolve brw --- src/Api/Controllers/StaffsController.cs | 1 + .../Staffs/Commands/RemoveStaffFromRoom.cs | 42 +++++++++ .../Commands/RemoveStaffFromRoomTests.cs | 87 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 7fdad81b..ecd2b9c8 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -102,6 +102,7 @@ public async Task>> Add([FromBody] AddStaffRequest [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveFromRoom( [FromRoute] Guid staffId) { diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index 607eb057..b4e8be49 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,5 +1,9 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Staffs.Commands; @@ -9,4 +13,42 @@ public record Command : IRequest { public Guid StaffId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.User.Id.Equals(request.StaffId), cancellationToken: cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff is not assigned to a room."); + } + + staff.Room.Staff = null; + _context.Rooms.Update(staff.Room!); + staff.Room = null; + var result = _context.Staffs.Update(staff); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs new file mode 100644 index 00000000..1246c7d8 --- /dev/null +++ b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs @@ -0,0 +1,87 @@ +using Application.Common.Exceptions; +using Application.Identity; +using Application.Staffs.Commands; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Staffs.Commands; + +public class RemoveStaffFromRoomTests : BaseClassFixture +{ + public RemoveStaffFromRoomTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUnasignStaff_WhenStaffHaveARoom() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + var user = CreateUser(IdentityData.Roles.Admin, "123123"); + var staff = CreateStaff(user, room); + await AddAsync(staff); + + var command = new RemoveStaffFromRoom.Command() + { + StaffId = staff.Id + }; + + // Act + var result = await SendAsync(command); + + // Assert + var assertionRoom = await FindAsync(room.Id); + result.Room.Should().BeNull(); + assertionRoom.Staff.Should().BeNull(); + + // Cleanup + Remove(staff); + Remove(await FindAsync(user.Id)); + Remove(await FindAsync(room.Id)); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() + { + // Arrange + var command = new RemoveStaffFromRoom.Command() + { + StaffId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync("Staff does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenStaffIsNotAssignedToARoom() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Admin,"123123"); + var staff = CreateStaff(user, null); + await AddAsync(staff); + + var command = new RemoveStaffFromRoom.Command() + { + StaffId = staff.Id + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Staff is not assigned to a room."); + + // Cleanup + Remove(staff); + Remove(await FindAsync(user.Id)); + } +} \ No newline at end of file From 4df6d616c37e6ecb0e30055ffe5936191b7f4fb6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:44:24 +0700 Subject: [PATCH 034/162] Feat/reset password email (#145) * add: user created event handler * add: email handling with mailtrap * add: temporary password when create user * fix: add custom mail service --- .../Payload/Requests/Users/AddUserRequest.cs | 4 -- src/Api/Controllers/UsersController.cs | 1 - src/Api/Program.cs | 1 - src/Api/appsettings.Development.json | 7 +++ src/Application/Application.csproj | 5 +- .../Common/Interfaces/IMailService.cs | 8 +++ src/Application/Common/Models/HTMLMailData.cs | 39 +++++++++++++ src/Application/Helpers/StringUtil.cs | 19 +++++++ src/Application/Users/Commands/AddUser.cs | 9 +-- .../EventHandlers/UserCreatedEventHandler.cs | 20 +++++++ src/Domain/Events/UserCreatedEvent.cs | 8 ++- src/Infrastructure/ConfigureServices.cs | 24 ++++++-- src/Infrastructure/Infrastructure.csproj | 1 + src/Infrastructure/Services/MailService.cs | 57 +++++++++++++++++++ src/Infrastructure/Shared/MailSettings.cs | 10 ++++ .../CustomApiFactory.cs | 26 ++++++--- .../CustomMailService.cs | 11 ++++ .../Users/Commands/AddUserTests.cs | 1 - 18 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 src/Application/Common/Interfaces/IMailService.cs create mode 100644 src/Application/Common/Models/HTMLMailData.cs create mode 100644 src/Application/Helpers/StringUtil.cs create mode 100644 src/Application/Users/EventHandlers/UserCreatedEventHandler.cs create mode 100644 src/Infrastructure/Services/MailService.cs create mode 100644 src/Infrastructure/Shared/MailSettings.cs create mode 100644 tests/Application.Tests.Integration/CustomMailService.cs diff --git a/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs index 2c449aff..ad129e16 100644 --- a/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs @@ -14,10 +14,6 @@ public class AddUserRequest /// public string Email { get; init; } = null!; /// - /// Password of the user to be added - /// - public string Password { get; init; } = null!; - /// /// First name of the user to be added /// public string? FirstName { get; init; } diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index cc0c4ccc..3ca0edce 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -71,7 +71,6 @@ public async Task>> Add([FromBody] AddUserRequest r { Username = request.Username, Email = request.Email, - Password = request.Password, FirstName = request.FirstName, LastName = request.LastName, Role = request.Role, diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 829f0873..5bd6b849 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -2,7 +2,6 @@ using Api.Extensions; using Application; using Infrastructure; -using Infrastructure.Persistence; using Serilog; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 41b82277..ef1296b0 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -9,6 +9,13 @@ "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, + "MailSettings": { + "ClientUrl": "https://send.api.mailtrap.io/api/send", + "Token": "745f040659edff0ce87b545567da72d2", + "SenderName": "ProFile", + "SenderEmail": "profile@ezarp.dev", + "TemplateUuid": "9d6a8f25-65e9-4819-be7d-106ce077acf1" + }, "Seed": true, "Serilog" : { "MinimumLevel" : { diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index ecc9e09f..f1789974 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -11,6 +11,7 @@ + @@ -19,8 +20,4 @@ - - - - diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs new file mode 100644 index 00000000..e7fe9d67 --- /dev/null +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -0,0 +1,8 @@ +using Application.Common.Models; + +namespace Application.Common.Interfaces; + +public interface IMailService +{ + bool SendResetPasswordHtmlMail(string userEmail, string password); +} \ No newline at end of file diff --git a/src/Application/Common/Models/HTMLMailData.cs b/src/Application/Common/Models/HTMLMailData.cs new file mode 100644 index 00000000..edc8dae0 --- /dev/null +++ b/src/Application/Common/Models/HTMLMailData.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Application.Common.Models; + +public class HTMLMailData +{ + [JsonPropertyName("from")] + public From From { get; set; } + [JsonPropertyName("to")] + public To[] To { get; set; } + [JsonPropertyName("template_uuid")] + public string TemplateUuid { get; set; } + [JsonPropertyName("template_variables")] + public TemplateVariables TemplateVariables { get; set; } +} + +public class From +{ + [JsonPropertyName("email")] + public string Email { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } +} + +public class To +{ + [JsonPropertyName("email")] + public string Email { get; set; } +} + +public class TemplateVariables +{ + [JsonPropertyName("user_email")] + public string UserEmail { get; set; } + [JsonPropertyName("pass_reset_link")] + public string PassResetLink { get; set; } + [JsonPropertyName("user_password")] + public string UserPassword { get; set; } +} \ No newline at end of file diff --git a/src/Application/Helpers/StringUtil.cs b/src/Application/Helpers/StringUtil.cs new file mode 100644 index 00000000..09f68c24 --- /dev/null +++ b/src/Application/Helpers/StringUtil.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace Application.Helpers; + +public class StringUtil +{ + private static string alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + public static string RandomString(int n) + { + var stringBuilder = new StringBuilder(); + var length = alphanumeric.Length; + for (var i = 0; i < n; i++) + { + stringBuilder.Append(alphanumeric.ElementAt(new Random().Next(0, length))); + } + + return stringBuilder.ToString(); + } +} \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 448b8c08..52ca1f80 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -30,9 +30,6 @@ public Validator() .EmailAddress().WithMessage("Require valid email.") .MaximumLength(320).WithMessage("Email length too long."); - RuleFor(x => x.Password) - .NotEmpty().WithMessage("Password is required."); - RuleFor(x => x.Role) .NotEmpty().WithMessage("Role is required.") .MaximumLength(64).WithMessage("Role cannot exceed 64 characters.") @@ -58,7 +55,6 @@ public record Command : IRequest { public string Username { get; init; } = null!; public string Email { get; init; } = null!; - public string Password { get; init; } = null!; public string? FirstName { get; init; } public string? LastName { get; init; } public Guid DepartmentId { get; init; } @@ -95,10 +91,11 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Department does not exist."); } + var password = StringUtil.RandomString(8); var entity = new User { Username = request.Username, - PasswordHash = SecurityUtil.Hash(request.Password), + PasswordHash = SecurityUtil.Hash(password), Email = request.Email, FirstName = request.FirstName?.Trim(), LastName = request.LastName?.Trim(), @@ -109,7 +106,7 @@ public async Task Handle(Command request, CancellationToken cancellatio IsActivated = false, Created = LocalDateTime.FromDateTime(DateTime.UtcNow) }; - entity.AddDomainEvent(new UserCreatedEvent(entity)); + entity.AddDomainEvent(new UserCreatedEvent(entity.Email, password)); var result = await _context.Users.AddAsync(entity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs new file mode 100644 index 00000000..7c2efe9b --- /dev/null +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -0,0 +1,20 @@ +using Application.Common.Interfaces; +using Domain.Events; +using MediatR; + +namespace Application.Users.EventHandlers; + +public class UserCreatedEventHandler : INotificationHandler +{ + private readonly IMailService _mailService; + + public UserCreatedEventHandler(IMailService mailService) + { + _mailService = mailService; + } + + public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) + { + _mailService.SendResetPasswordHtmlMail(notification.Email, notification.Password); + } +} \ No newline at end of file diff --git a/src/Domain/Events/UserCreatedEvent.cs b/src/Domain/Events/UserCreatedEvent.cs index 341553c3..ee1e507e 100644 --- a/src/Domain/Events/UserCreatedEvent.cs +++ b/src/Domain/Events/UserCreatedEvent.cs @@ -5,10 +5,12 @@ namespace Domain.Events; public class UserCreatedEvent : BaseEvent { - public UserCreatedEvent(User user) + public UserCreatedEvent(string email, string password) { - User = user; + Email = email; + Password = password; } - public User User { get; } + public string Email { get; } + public string Password { get; } } \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index bd1bede7..d3820329 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -1,13 +1,10 @@ -using System.Data; using System.Security.Cryptography; -using System.Text; using Application.Common.Interfaces; using Infrastructure.Identity; using Infrastructure.Identity.Authentication; using Infrastructure.Persistence; +using Infrastructure.Services; using Infrastructure.Shared; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -22,6 +19,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddApplicationDbContext(configuration); services.AddScoped(); services.AddScoped(); + services.AddMailService(configuration); services.AddJweAuthentication(configuration); @@ -92,4 +90,22 @@ private static IServiceCollection AddJweAuthentication(this IServiceCollection s return services; } + + private static IServiceCollection AddMailService(this IServiceCollection services, IConfiguration configuration) + { + var mailSettings = configuration.GetSection(nameof(MailSettings)).Get(); + + services.Configure(options => + { + options.ClientUrl = mailSettings!.ClientUrl; + options.Token = mailSettings!.Token; + options.SenderEmail = mailSettings!.SenderEmail; + options.SenderName = mailSettings!.SenderName; + options.TemplateUuid = mailSettings!.TemplateUuid; + }); + + services.AddTransient(); + + return services; + } } \ No newline at end of file diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 8537748f..8b76879b 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs new file mode 100644 index 00000000..8357655f --- /dev/null +++ b/src/Infrastructure/Services/MailService.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Application.Common.Interfaces; +using Application.Common.Models; +using Infrastructure.Shared; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using RestSharp; + +namespace Infrastructure.Services; + +public class MailService : IMailService +{ + private readonly MailSettings _mailSettings; + + public MailService(IOptions mailSettingsOptions) + { + _mailSettings = mailSettingsOptions.Value; + } + + public bool SendResetPasswordHtmlMail(string userEmail, string password) + { + var data = new HTMLMailData() + { + From = new From() + { + Email = _mailSettings.SenderEmail, + Name = _mailSettings.SenderName, + }, + To = new To[] + { + new (){ Email = userEmail }, + }, + TemplateUuid = _mailSettings.TemplateUuid, + TemplateVariables = new TemplateVariables() + { + UserEmail = userEmail, + PassResetLink = "random_shit", + UserPassword = password, + }, + }; + + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + var client = new RestClient(_mailSettings.ClientUrl); + var request = new RestRequest + { + Method = Method.Post + }; + + request.AddHeader("Authorization", $"{JwtBearerDefaults.AuthenticationScheme} {_mailSettings.Token}"); + request.AddHeader("Content-Type", "application/json"); + request.AddParameter("application/json", json, ParameterType.RequestBody); + var response = client.Execute(request); + + return response.IsSuccessStatusCode; + } +} \ No newline at end of file diff --git a/src/Infrastructure/Shared/MailSettings.cs b/src/Infrastructure/Shared/MailSettings.cs new file mode 100644 index 00000000..2e139992 --- /dev/null +++ b/src/Infrastructure/Shared/MailSettings.cs @@ -0,0 +1,10 @@ +namespace Infrastructure.Shared; + +public class MailSettings +{ + public string ClientUrl { get; set; } + public string Token { get; set; } + public string SenderName { get; set; } + public string SenderEmail { get; set; } + public string TemplateUuid { get; set; } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/CustomApiFactory.cs b/tests/Application.Tests.Integration/CustomApiFactory.cs index f76d4ac3..0ec3bd9f 100644 --- a/tests/Application.Tests.Integration/CustomApiFactory.cs +++ b/tests/Application.Tests.Integration/CustomApiFactory.cs @@ -1,4 +1,5 @@ using Api; +using Application.Common.Interfaces; using Infrastructure.Persistence; using Infrastructure.Shared; using Microsoft.AspNetCore.Hosting; @@ -16,23 +17,32 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices((builderContext, services) => { - var descriptor = services.SingleOrDefault( - d => d.ServiceType == - typeof(DbContextOptions)); - - if (descriptor != null) - { - services.Remove(descriptor); - } + Remove>(services); var databaseSettings = GetConfiguration().GetSection(nameof(DatabaseSettings)).Get(); services.AddDbContext(options => { options.UseNpgsql(databaseSettings!.ConnectionString, optionsBuilder => optionsBuilder.UseNodaTime()); }); + + Remove(services); + services.AddTransient(); }); } + private static void Remove(IServiceCollection services) + where T : class + { + var descriptor = services.SingleOrDefault( + d => d.ServiceType == + typeof(T)); + + if (descriptor != null) + { + services.Remove(descriptor); + } + } + private IConfiguration GetConfiguration() { return new ConfigurationBuilder() diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs new file mode 100644 index 00000000..e439bf07 --- /dev/null +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -0,0 +1,11 @@ +using Application.Common.Interfaces; + +namespace Application.Tests.Integration; + +public class CustomMailService : IMailService +{ + public bool SendResetPasswordHtmlMail(string userEmail, string password) + { + return true; + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs index 89842090..d12973bf 100644 --- a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs +++ b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs @@ -25,7 +25,6 @@ public async Task ShouldCreateUser_WhenCreateDetailsAreValid() Email = new Faker().Person.Email, FirstName = new Faker().Person.FirstName, LastName = new Faker().Person.LastName, - Password = new Faker().Random.Word(), Role = new Faker().Random.Word(), DepartmentId = department.Id, Position = new Faker().Random.Word(), From 10e786ebc12d8747f871a1d624c74dca85474f0f Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:46:45 +0700 Subject: [PATCH 035/162] feat: get staff by room + tests (#151) * feat: get staff by room + tests * refactor: refactor test for clearance --- .../Staffs/Queries/GetStaffByRoom.cs | 36 +++++++ .../Staffs/Queries/GetStaffByRoomTests.cs | 97 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs diff --git a/src/Application/Staffs/Queries/GetStaffByRoom.cs b/src/Application/Staffs/Queries/GetStaffByRoom.cs index 9f1ac07b..60db1c83 100644 --- a/src/Application/Staffs/Queries/GetStaffByRoom.cs +++ b/src/Application/Staffs/Queries/GetStaffByRoom.cs @@ -1,5 +1,8 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Staffs.Queries; @@ -9,4 +12,37 @@ public record Query : IRequest { public Guid RoomId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) + .ThenInclude(x => x.User) + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + if (room.Staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + return _mapper.Map(room.Staff); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs new file mode 100644 index 00000000..123e2542 --- /dev/null +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs @@ -0,0 +1,97 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Application.Staffs.Queries; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Staffs.Queries; + +public class GetStaffByRoomTests : BaseClassFixture +{ + private readonly IMapper _mapper; + + public GetStaffByRoomTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnStaff_WhenRoomHaveStaff() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + var user = CreateUser(IdentityData.Roles.Staff, "123123"); + var staff = CreateStaff(user, room); + await AddAsync(staff); + + var query = new GetStaffByRoom.Query() + { + RoomId = room.Id + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Should().BeEquivalentTo(_mapper.Map(staff), + config => config.Excluding(x => x.User.Created)); + result.User.Should().BeEquivalentTo(_mapper.Map(user), + config => config.Excluding(x => x.Created)); + result.Room.Should().BeEquivalentTo(_mapper.Map(room)); + + // Cleanup + Remove(staff); + Remove(await FindAsync(user.Id)); + Remove(await FindAsync(room.Id)); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() + { + // Arrange + var query = new GetStaffByRoom.Query() + { + RoomId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Room does not exist."); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotHaveStaff() + { + // Arrange + var department = CreateDepartment(); + var room = CreateRoom(department); + await AddAsync(room); + + var query = new GetStaffByRoom.Query() + { + RoomId = room.Id + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Staff does not exist."); + + // Cleanup + Remove(room); + Remove(await FindAsync(department.Id)); + } +} \ No newline at end of file From 6f2d1d13a754cf64a031ea9edd018352579a8595 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:01:16 +0700 Subject: [PATCH 036/162] add: integration test and implementation (#138) * add: integration test and implementation * fix: pagination and search trim --- src/Api/Controllers/UsersController.cs | 1 + .../Users/Queries/GetAllUsersPaginated.cs | 55 +++++++++++++++++++ .../Queries/GetAllUsersPaginatedTests.cs | 43 +++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/Application.Tests.Integration/Users/Queries/GetAllUsersPaginatedTests.cs diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 3ca0edce..5b8efe5f 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -34,6 +34,7 @@ public async Task>> GetById([FromRoute] Guid userId /// /// Get all users query parameters /// A paginated list of UserDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index 738ed5ea..e68b7f20 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -1,5 +1,13 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using AutoMapper; +using AutoMapper.QueryableExtensions; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Users.Queries; @@ -14,4 +22,51 @@ public record Query : IRequest> public string? SortBy { get; init; } public string? SortOrder { get; init; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var users = _context.Users.AsQueryable() + .Where(x => !x.Role.Equals(IdentityData.Roles.Admin)); + + if (request.DepartmentId is not null) + { + users = users.Where(x => x.Department!.Id == request.DepartmentId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + users = users.Where(x => + x.FirstName!.ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(UserDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await users + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Queries/GetAllUsersPaginatedTests.cs b/tests/Application.Tests.Integration/Users/Queries/GetAllUsersPaginatedTests.cs new file mode 100644 index 00000000..0133c725 --- /dev/null +++ b/tests/Application.Tests.Integration/Users/Queries/GetAllUsersPaginatedTests.cs @@ -0,0 +1,43 @@ +using Application.Common.Mappings; +using Application.Identity; +using Application.Users.Queries; +using AutoMapper; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Users.Queries; + +public class GetAllUsersPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllUsersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnUsers() + { + // Arrange + var user1 = CreateUser(IdentityData.Roles.Admin, "admin"); + var user2 = CreateUser(IdentityData.Roles.Employee, "employee"); + await AddAsync(user1); + await AddAsync(user2); + + var query = new GetAllUsersPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(1); + result.Items.Should().ContainEquivalentOf(_mapper.Map(user2), config => config.Excluding(x => x.Created)); + result.Items.Should().NotContainEquivalentOf(_mapper.Map(user1), config => config.Excluding(x => x.Created)); + + // Cleanup + Remove(user1); + Remove(user2); + } +} \ No newline at end of file From 207b954ff1025053578e226aea033970f8e93073 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:01:53 +0700 Subject: [PATCH 037/162] Feat/get all documents (#137) * update: get all documents now return DocumentDto instead of DocumentItemDto * forgot to ship some file * add: search term * im sorry * fix: search term untranslatable --- .../Queries/GetAllDocumentsPaginated.cs | 23 ++++++++----------- .../Queries/GetAllDocumentsPaginatedTests.cs | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index e3695cb9..d2d4f38b 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -21,20 +21,9 @@ public Validator() RuleLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.RoomId) - .Must((query, roomId) => - { - if (roomId is null) - { - return query.LockerId is null && query.FolderId is null; - } - - if (query.LockerId is null) - { - return query.FolderId is null; - } - - return true; - }).WithMessage("Container orientation is not consistent"); + .Must((query, roomId) => roomId is null + ? query.LockerId is null && query.FolderId is null + : query.LockerId is not null || query.FolderId is null).WithMessage("Container orientation is not consistent"); } } @@ -127,6 +116,12 @@ public async Task> Handle(Query request, documents = documents.Where(x => x.Folder!.Locker.Room.Id == request.RoomId); } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) diff --git a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs index 3863d1f4..6410ec75 100644 --- a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs @@ -193,7 +193,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdAndLockerIdArePr // Act var action = async () => await SendAsync(query); - + // Assert await action.Should().ThrowAsync("Locker does not exist."); From c4de1cc6d12738d3d8dc609ef1389faa893a4566 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:54:56 +0700 Subject: [PATCH 038/162] fix: import document (#168) --- src/Application/Documents/Commands/ImportDocument.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 2f5e8c02..16e9c6a7 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -56,6 +56,11 @@ public async Task Handle(Command request, CancellationToken cancell throw new KeyNotFoundException("Folder does not exist."); } + if (folder.Capacity == folder.NumberOfDocuments) + { + throw new ConflictException("This folder cannot accept more documents."); + } + var entity = new Document() { Title = request.Title.Trim(), From 5d0b657b9de15e62fc19a2be7d0ae451c15a364f Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Fri, 2 Jun 2023 22:45:55 +0700 Subject: [PATCH 039/162] Feat/delete document (#163) * add: integration test * feat: add delete document feature * refactor: remove unnecessary check ? * I hate this shitgit add .! Plss refactor those testsgit add .! --------- Co-authored-by: Nguyen Quang Chien --- .../Documents/Commands/DeleteDocument.cs | 39 ++++++++++++++ .../Documents/Commands/DeleteDocumentTests.cs | 52 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 7b22f634..903b7804 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,5 +1,8 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Documents.Commands; @@ -9,4 +12,40 @@ public record Command : IRequest { public Guid DocumentId { get; init; } } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include( x => x.Folder) + .FirstOrDefaultAsync(x => x.Id.Equals(request.DocumentId), cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + var folder = document.Folder; + var result = _context.Documents.Remove(document); + + if (folder is not null) + { + folder.NumberOfDocuments -= 1; + _context.Folders.Update(folder); + } + + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs new file mode 100644 index 00000000..1765a2b0 --- /dev/null +++ b/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs @@ -0,0 +1,52 @@ +using Application.Documents.Commands; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Documents.Commands; + +public class DeleteDocumentTests : BaseClassFixture +{ + public DeleteDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldDeleteDocument_WhenThatDocumentExists() + { + // Arrange + var document = CreateNDocuments(1).First(); + await AddAsync(document); + + var command = new DeleteDocument.Command() + { + DocumentId = document.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(document.Id); + result.Title.Should().Be(document.Title); + var deletedDocument = await FindAsync(document.Id); + deletedDocument.Should().BeNull(); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatDocumentDoesNotExist() + { + // Arrange + var command = new DeleteDocument.Command() + { + DocumentId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Document does not exist."); + } +} \ No newline at end of file From 31e488c96b3e0f896e5b02c4366e8ff834a3f06d Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:15:19 +0700 Subject: [PATCH 040/162] feat: get staff by id (#173) --- .../Staffs/Queries/GetStaffById.cs | 30 +++++++++ .../Staffs/Queries/GetStaffByIdTests.cs | 61 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/Application.Tests.Integration/Staffs/Queries/GetStaffByIdTests.cs diff --git a/src/Application/Staffs/Queries/GetStaffById.cs b/src/Application/Staffs/Queries/GetStaffById.cs index 7510235e..6cab8c90 100644 --- a/src/Application/Staffs/Queries/GetStaffById.cs +++ b/src/Application/Staffs/Queries/GetStaffById.cs @@ -1,5 +1,8 @@ +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Staffs.Queries; @@ -9,4 +12,31 @@ public record Query : IRequest { public Guid StaffId { get; init; } } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .Include(x => x.User) + .Include(y => y.Room) + .FirstOrDefaultAsync(x => x.Id.Equals(request.StaffId), cancellationToken: cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + return _mapper.Map(staff); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByIdTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByIdTests.cs new file mode 100644 index 00000000..ac11a39a --- /dev/null +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByIdTests.cs @@ -0,0 +1,61 @@ +using Application.Identity; +using Application.Staffs.Queries; +using Domain.Entities; +using Domain.Entities.Physical; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Staffs.Queries; + +public class GetStaffByIdTests : BaseClassFixture +{ + public GetStaffByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnStaff_WhenIdIsValid() + { + // Arrange + var department = CreateDepartment(); + var user = CreateUser(IdentityData.Roles.Staff, "123123"); + var room = CreateRoom(department); + var staff = CreateStaff(user, room); + await AddAsync(staff); + + var query = new GetStaffById.Query() + { + StaffId = staff.Id + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.User.Id.Should().Be(staff.Id); + + // Cleanup + Remove(staff); + Remove(await FindAsync(room.Id)); + Remove(await FindAsync(user.Id)); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() + { + // Arrange + var query = new GetStaffById.Query() + { + StaffId = Guid.NewGuid() + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Staff does not exist."); + } + +} \ No newline at end of file From ddf6f5288b7e9e0a637c4b4ea44806b0d2142f22 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:56:16 +0700 Subject: [PATCH 041/162] Feat/borrow (#159) * add: base * add: request borrow * add: a couple more things * add: base for borrow * update: divide get all borrow requests as admin and staff * just update my progress here * update once more? * add: base ground for borrow endpoints * add: validation and fix simple logic in borrowing document * refactor: a bit * final touch * fix my wholie life * jk im good --- docker-compose.test.yml | 2 +- src/Api/ConfigureServices.cs | 3 + src/Api/Controllers/BorrowsController.cs | 264 ++++++++++++++++++ src/Api/Controllers/DocumentsController.cs | 4 +- .../Requests/Borrows/BorrowDocumentRequest.cs | 24 ++ ...RequestsPaginatedAsAdminQueryParameters.cs | 14 + ...uestsPaginatedAsEmployeeQueryParameters.cs | 9 + ...RequestsPaginatedAsStaffQueryParameters.cs | 10 + ...estsPaginatedForDocumentQueryParameters.cs | 6 + .../Requests/Borrows/UpdateBorrowRequest.cs | 8 + ...GetAllDocumentsPaginatedQueryParameters.cs | 18 +- .../GetAllFoldersPaginatedQueryParameters.cs | 26 +- .../GetAllLockersPaginatedQueryParameters.cs | 18 +- .../Requests/PaginatedQueryParameters.cs | 21 ++ .../GetAllRoomsPaginatedQueryParameters.cs | 18 +- .../GetAllStaffsPaginatedQueryParameters.cs | 18 +- .../GetAllUsersPaginatedQueryParameters.cs | 18 +- src/Api/Controllers/RoomsController.cs | 2 +- src/Api/Program.cs | 1 - src/Api/Services/CurrentUserService.cs | 72 ++++- .../Borrows/Commands/ApproveBorrowRequest.cs | 80 ++++++ .../Borrows/Commands/BorrowDocument.cs | 138 +++++++++ .../Borrows/Commands/CancelBorrowRequest.cs | 51 ++++ .../Borrows/Commands/CheckoutDocument.cs | 58 ++++ .../Borrows/Commands/RejectBorrowRequest.cs | 50 ++++ .../Borrows/Commands/ReturnDocument.cs | 61 ++++ .../Borrows/Commands/UpdateBorrow.cs | 104 +++++++ .../Queries/GetAllBorrowRequestsPaginated.cs | 94 +++++++ .../GetAllBorrowRequestsSpecificPaginated.cs | 79 ++++++ .../Common/Extensions/QueryableExtensions.cs | 2 +- .../Common/Interfaces/ICurrentUserService.cs | 5 + .../Common/Models/Dtos/Physical/BorrowDto.cs | 18 +- .../Models/Dtos/Physical/DocumentDto.cs | 9 + .../Documents/Commands/ImportDocument.cs | 4 +- .../Queries/GetAllDocumentsPaginated.cs | 6 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 4 +- .../Lockers/Queries/GetAllLockersPaginated.cs | 2 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 2 +- src/Application/Staffs/Commands/AddStaff.cs | 47 +++- src/Domain/Entities/Physical/Borrow.cs | 3 + src/Domain/Entities/Physical/Document.cs | 2 + src/Domain/Statuses/BorrowRequestStatus.cs | 14 + src/Domain/Statuses/DocumentStatus.cs | 9 + .../Identity/IdentityService.cs | 4 +- .../Configurations/BorrowConfiguration.cs | 3 + .../Configurations/DocumentConfiguration.cs | 3 + .../00000000000009_RoomMustHaveADepartment.cs | 22 -- ...> 00000000000010_UpdateBorrow.Designer.cs} | 13 +- .../Migrations/00000000000010_UpdateBorrow.cs | 124 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 9 + src/Infrastructure/Services/MailService.cs | 1 - .../Folders/Commands/DisableFolderTests.cs | 117 ++------ .../Lockers/Commands/AddLockerTests.cs | 63 ++--- 53 files changed, 1453 insertions(+), 304 deletions(-) create mode 100644 src/Api/Controllers/BorrowsController.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs create mode 100644 src/Application/Borrows/Commands/ApproveBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/BorrowDocument.cs create mode 100644 src/Application/Borrows/Commands/CancelBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/CheckoutDocument.cs create mode 100644 src/Application/Borrows/Commands/RejectBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/ReturnDocument.cs create mode 100644 src/Application/Borrows/Commands/UpdateBorrow.cs create mode 100644 src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs create mode 100644 src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs create mode 100644 src/Domain/Statuses/BorrowRequestStatus.cs create mode 100644 src/Domain/Statuses/DocumentStatus.cs delete mode 100644 src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs rename src/Infrastructure/Persistence/Migrations/{00000000000009_RoomMustHaveADepartment.Designer.cs => 00000000000010_UpdateBorrow.Designer.cs} (97%) create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 993b5ad1..a1e8f565 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -9,7 +9,7 @@ services: - '8888:80' environment: - ASPNETCORE_ENVIRONMENT=Testing - - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured; + - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured;Include Error Detail=true depends_on: database: condition: service_started diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 39dabb10..4c4f747f 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -1,6 +1,8 @@ using System.Reflection; using Api.Middlewares; using Api.Policies; +using Api.Services; +using Application.Common.Interfaces; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.OpenApi.Models; @@ -53,6 +55,7 @@ private static IServiceCollection AddServices(this IServiceCollection services) { // In order for ExceptionMiddleware to work services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs new file mode 100644 index 00000000..0c7b34c1 --- /dev/null +++ b/src/Api/Controllers/BorrowsController.cs @@ -0,0 +1,264 @@ +using Api.Controllers.Payload.Requests.Borrows; +using Application.Borrows.Commands; +using Application.Borrows.Queries; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class BorrowsController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + public BorrowsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// + /// Borrow a document request + /// + /// Borrow document details + /// A BorrowDto of the requested borrow + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> BorrowDocument([FromBody] BorrowDocumentRequest request) + { + var borrowerId = _currentUserService.GetCurrentUser().Id; + var command = new BorrowDocument.Command() + { + BorrowerId = borrowerId, + DocumentId = request.DocumentId, + BorrowFrom = request.BorrowFrom, + BorrowTo = request.BorrowTo, + Reason = request.Reason, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("staffs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsAsStaffPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); + var command = new GetAllBorrowRequestsPaginated.Query() + { + DepartmentId = departmentId, + EmployeeId = queryParameters.EmployeeId, + DocumentId = queryParameters.DocumentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all borrow requests as admin paginated + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsAsAdminPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters queryParameters) + { + var command = new GetAllBorrowRequestsPaginated.Query() + { + DepartmentId = queryParameters.DepartmentId, + EmployeeId = queryParameters.EmployeeId, + DocumentId = queryParameters.DocumentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all borrow requests as employee paginated + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsAsEmployeePaginated( + [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) + { + var userId = _currentUserService.GetCurrentUser().Id; + var command = new GetAllBorrowRequestsPaginated.Query() + { + EmployeeId = userId, + DocumentId = queryParameters.DocumentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all borrow requests for a document paginated + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("documents/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllRequestsForDocumentPaginated( + [FromRoute] Guid documentId, + [FromQuery] GetAllBorrowRequestsPaginatedForDocumentQueryParameters queryParameters) + { + var command = new GetAllBorrowRequestsPaginated.Query() + { + DocumentId = documentId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } + + /// + /// Approve a borrow request + /// + /// Id of the borrow request to be approved + /// A BorrowDto of the approved borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("approve/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> ApproveRequest([FromRoute] Guid borrowId) + { + var command = new ApproveBorrowRequest.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Reject a borrow request + /// + /// Id of the borrow request to be rejected + /// A BorrowDto of the rejected borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("reject/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RejectRequest([FromRoute] Guid borrowId) + { + var command = new RejectBorrowRequest.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Check out a borrow request + /// + /// Id of the borrow request to be checked out + /// A BorrowDto of the checked out borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("checkout/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkout([FromRoute] Guid borrowId) + { + var command = new CheckoutDocument.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Return document + /// + /// Id of the document to be returned + /// A BorrowDto of the returned document borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("return/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Return([FromRoute] Guid documentId) + { + var command = new ReturnDocument.Command() + { + DocumentId = documentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Update borrow request + /// + /// Id of the borrow request to be updated + /// Update borrow details + /// A BorrowDto of the updated borrow request + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPut("{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update( + [FromRoute] Guid borrowId, + [FromBody] UpdateBorrowRequest request) + { + var command = new UpdateBorrow.Command() + { + BorrowId = borrowId, + BorrowFrom = request.BorrowFrom, + BorrowTo = request.BorrowTo, + Reason = request.Reason, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 5c56986b..8d817d70 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -105,7 +105,7 @@ public async Task>> Import([FromBody] ImportDoc /// Id of the document to be updated /// Update document details /// A DocumentDto of the updated document - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -128,7 +128,7 @@ public async Task>> Update([FromRoute] Guid doc /// Delete a document /// /// Id of the document to be deleted - /// A DocumentDto of the deleted document + /// A DocumentDto of the deleted document [HttpDelete("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs new file mode 100644 index 00000000..f3a0085d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/BorrowDocumentRequest.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Request details to borrow a document +/// +public class BorrowDocumentRequest +{ + /// + /// Id of the document to be borrowed + /// + public Guid DocumentId { get; set; } + /// + /// Borrow from + /// + public DateTime BorrowFrom { get; set; } + /// + /// Borrow to + /// + public DateTime BorrowTo { get; set; } + /// + /// Reason of borrowing + /// + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs new file mode 100644 index 00000000..8ea8eca1 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs @@ -0,0 +1,14 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Query parameters for getting all borrow requests with pagination as admin +/// +public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the department to get borrow requests in + /// + public Guid? DepartmentId { get; set; } + public Guid? DocumentId { get; set; } + public Guid? EmployeeId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs new file mode 100644 index 00000000..922651ed --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Query parameters for getting all borrow requests with pagination as employee +/// +public class GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters : PaginatedQueryParameters +{ + public Guid? DocumentId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs new file mode 100644 index 00000000..d9656766 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsStaffQueryParameters.cs @@ -0,0 +1,10 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +/// +/// Query parameters for getting all borrow requests with pagination as staff +/// +public class GetAllBorrowRequestsPaginatedAsStaffQueryParameters : PaginatedQueryParameters +{ + public Guid? DocumentId { get; set; } + public Guid? EmployeeId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs new file mode 100644 index 00000000..a44619fe --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class GetAllBorrowRequestsPaginatedForDocumentQueryParameters : PaginatedQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs new file mode 100644 index 00000000..dbbef730 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/UpdateBorrowRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class UpdateBorrowRequest +{ + public DateTime BorrowFrom { get; init; } + public DateTime BorrowTo { get; init; } + public string Reason { get; init; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index 7f134c04..7b97fc8f 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Payload.Requests.Documents; /// /// Query parameters for getting all documents with pagination /// -public class GetAllDocumentsPaginatedQueryParameters +public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the room to find documents in @@ -21,20 +21,4 @@ public class GetAllDocumentsPaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs index ae6399ab..298a30b0 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs @@ -3,8 +3,12 @@ namespace Api.Controllers.Payload.Requests.Folders; /// /// Query parameters for getting all folders with pagination /// -public class GetAllFoldersPaginatedQueryParameters +public class GetAllFoldersPaginatedQueryParameters : PaginatedQueryParameters { + /// + /// Search term + /// + public string? SearchTerm { get; set; } /// /// Id of the room to find folders in /// @@ -13,24 +17,4 @@ public class GetAllFoldersPaginatedQueryParameters /// Id of the locker to find folders in /// public Guid? LockerId { get; set; } - /// - /// Search term - /// - public string? SearchTerm { get; init; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs index 9f701ed4..25372261 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs @@ -5,7 +5,7 @@ namespace Api.Controllers.Payload.Requests.Lockers; /// /// Query parameters for getting all lockers with pagination /// -public class GetAllLockersPaginatedQueryParameters +public class GetAllLockersPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the room to find lockers in @@ -15,20 +15,4 @@ public class GetAllLockersPaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs new file mode 100644 index 00000000..d29c3838 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/PaginatedQueryParameters.cs @@ -0,0 +1,21 @@ +namespace Api.Controllers.Payload.Requests; + +public class PaginatedQueryParameters +{ + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } + /// + /// Sort criteria + /// + public string? SortBy { get; set; } + /// + /// Sort direction + /// + public string? SortOrder { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 89a7bbb6..390ae4be 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -3,26 +3,10 @@ namespace Api.Controllers.Payload.Requests.Rooms; /// /// Query parameters for getting all rooms with pagination /// -public class GetAllRoomsPaginatedQueryParameters +public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters { /// /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs index 53fcbb87..323ec479 100644 --- a/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs @@ -3,26 +3,10 @@ namespace Api.Controllers.Payload.Requests.Staffs; /// /// Query parameters for getting all staffs with pagination /// -public class GetAllStaffsPaginatedQueryParameters +public class GetAllStaffsPaginatedQueryParameters : PaginatedQueryParameters { /// /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 9f47d022..47db40c8 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Payload.Requests.Users; /// /// Query parameters for getting all users with pagination /// -public class GetAllUsersPaginatedQueryParameters +public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters { /// /// Id of the department to find users in @@ -13,20 +13,4 @@ public class GetAllUsersPaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } - /// - /// Page number - /// - public int? Page { get; set; } - /// - /// Size number - /// - public int? Size { get; set; } - /// - /// Sort criteria - /// - public string? SortBy { get; set; } - /// - /// Sort direction - /// - public string? SortOrder { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 5ecb848d..fa5198ee 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -61,7 +61,7 @@ public async Task>>> GetAllPaginated( /// Get empty containers paginated details /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("empty-containers")] + [HttpPost("empty-containers/{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 5bd6b849..2a739649 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -17,7 +17,6 @@ builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddApiServices(); - var app = builder.Build(); app.UseInfrastructure(builder.Configuration); diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index d8b6c481..0075bedb 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -1,5 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using Application.Common.Interfaces; +using Domain.Entities; +using Microsoft.EntityFrameworkCore; namespace Api.Services; @@ -17,7 +19,7 @@ public CurrentUserService(IHttpContextAccessor httpContextAccessor, IApplication public string GetRole() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -51,4 +53,72 @@ public string GetRole() return user.Department?.Name; } + + public User GetCurrentUser() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + + if (user is null) + { + throw new UnauthorizedAccessException(); + } + + return user; + } + + public Guid? GetCurrentRoomForStaff() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var staff = _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefault(x => x.User.Username.Equals(userName)); + + if (staff is null) + { + throw new UnauthorizedAccessException(); + } + + return staff.Room!.Id; + } + + public Guid? GetCurrentDepartmentForStaff() + { + var userName = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + if (userName is null) + { + throw new UnauthorizedAccessException(); + } + + var staff = _context.Staffs + .Include(x => x.User) + .Include(x => x.Room) + .FirstOrDefault(x => x.User.Username.Equals(userName)); + + if (staff is null) + { + throw new UnauthorizedAccessException(); + } + + if (staff.Room is null) + { + throw new UnauthorizedAccessException(); + } + + return staff.Room!.DepartmentId; + } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs new file mode 100644 index 00000000..9d7b345d --- /dev/null +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -0,0 +1,80 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ApproveBorrowRequest +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is DocumentStatus.Lost) + { + borrowRequest.Status = BorrowRequestStatus.NotProcessable; + _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + throw new ConflictException("Document is lost. Request is unprocessable."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending + && borrowRequest.Status is not BorrowRequestStatus.Rejected) + { + throw new ConflictException("Request cannot be approved."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .FirstOrDefaultAsync(x => + x.Document.Id == borrowRequest.Document.Id + && x.Id != borrowRequest.Id + && ((x.DueTime > localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + + if (existedBorrow is not null) + { + if (existedBorrow?.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && borrowRequest.BorrowTime < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } + } + + borrowRequest.Status = BorrowRequestStatus.Approved; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs new file mode 100644 index 00000000..602003bc --- /dev/null +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -0,0 +1,138 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class BorrowDocument +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Reason) + .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); + + RuleFor(x => x.BorrowFrom) + .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past.") + .Must((command, borrowTime) => borrowTime < command.BorrowTo).WithMessage("Due date cannot be before borrow date."); + + RuleFor(x => x.BorrowTo) + .GreaterThan(DateTime.Now).WithMessage("Due date cannot be in the past."); + } + } + + public record Command : IRequest + { + public Guid DocumentId { get; init; } + public Guid BorrowerId { get; init; } + public DateTime BorrowFrom { get; init; } + public DateTime BorrowTo { get; init; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var user = await _context.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.BorrowerId, cancellationToken); + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + if (user.IsActive is false) + { + throw new ConflictException("User is not active."); + } + + if (user.IsActivated is false) + { + throw new ConflictException("User is not activated."); + } + + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Available) + { + throw new ConflictException("Document is not available."); + } + + if (document.Department!.Id != user.Department!.Id) + { + throw new ConflictException("User is not allowed to borrow this document."); + } + + // getting out a request of that document which is either not due or overdue + // if the request is in time, meaning not overdue, + // then check if its due date is less than the borrow request date, if not then check + // if it's already been approved, checked out or lost, meaning + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .Include(x => x.Borrower) + .FirstOrDefaultAsync(x => + x.Document.Id == request.DocumentId + && ((x.DueTime > localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + + if (existedBorrow is not null) + { + // Does not make sense if the same person go up and want to borrow the same document again + // even if the borrow day will be after the due day + if (existedBorrow.Borrower.Id == request.BorrowerId) + { + throw new ConflictException("This document is already requested borrow from the same user."); + } + + if (existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } + } + + var entity = new Borrow() + { + Borrower = user, + Document = document, + BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom), + DueTime = LocalDateTime.FromDateTime(request.BorrowTo), + Reason = request.Reason, + Status = BorrowRequestStatus.Pending, + }; + + var result = await _context.Borrows.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs new file mode 100644 index 00000000..97f111d4 --- /dev/null +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -0,0 +1,51 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Commands; + +public class CancelBorrowRequest +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not (BorrowRequestStatus.Approved or BorrowRequestStatus.Pending)) + { + throw new ConflictException("Request cannot be cancelled."); + } + + borrowRequest.Status = BorrowRequestStatus.Cancelled; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs new file mode 100644 index 00000000..6578225f --- /dev/null +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -0,0 +1,58 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Commands; + +public class CheckoutDocument +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Available) + { + throw new ConflictException("Document is not available."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Approved) + { + throw new ConflictException("Request cannot be checked out."); + } + + borrowRequest.Status = BorrowRequestStatus.CheckedOut; + borrowRequest.Document.Status = DocumentStatus.Borrowed; + var result = _context.Borrows.Update(borrowRequest); + _context.Documents.Update(borrowRequest.Document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs new file mode 100644 index 00000000..e1290e83 --- /dev/null +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -0,0 +1,50 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Commands; + +public class RejectBorrowRequest +{ + public record Command : IRequest + { + public Guid BorrowId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending) + { + throw new ConflictException("Request cannot be rejected."); + } + + borrowRequest.Status = BorrowRequestStatus.Rejected; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs new file mode 100644 index 00000000..918a979d --- /dev/null +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -0,0 +1,61 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ReturnDocument +{ + public record Command : IRequest + { + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId + && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Borrowed) + { + throw new ConflictException("Document is not borrowed."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.CheckedOut) + { + throw new ConflictException("Request cannot be made."); + } + + borrowRequest.Status = BorrowRequestStatus.Returned; + borrowRequest.Document.Status = DocumentStatus.Available; + borrowRequest.ActualReturnTime = LocalDateTime.FromDateTime(DateTime.Now); + var result = _context.Borrows.Update(borrowRequest); + _context.Documents.Update(borrowRequest.Document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs new file mode 100644 index 00000000..fe39c471 --- /dev/null +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -0,0 +1,104 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class UpdateBorrow +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Reason) + .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); + + RuleFor(x => x.BorrowFrom) + .GreaterThan(DateTime.Now).WithMessage("Borrow date cannot be in the past.") + .Must((command, borrowTime) => borrowTime < command.BorrowTo).WithMessage("Due date cannot be before borrow date."); + + RuleFor(x => x.BorrowTo) + .GreaterThan(DateTime.Now).WithMessage("Due date cannot be in the past."); + } + } + + public record Command : IRequest + { + public Guid BorrowId { get; init; } + public DateTime BorrowFrom { get; init; } + public DateTime BorrowTo { get; init; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending) + { + throw new ConflictException("Cannot update borrow request."); + } + + if (borrowRequest.Document.Status is DocumentStatus.Lost) + { + throw new ConflictException("Document is lost."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var existedBorrow = await _context.Borrows + .Include(x => x.Borrower) + .FirstOrDefaultAsync(x => + x.Document.Id == borrowRequest.Document.Id + && x.Id != borrowRequest.Id + && ((x.DueTime > localDateTimeNow) + || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + + if (existedBorrow is not null) + { + if (existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } + } + + borrowRequest.BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom); + borrowRequest.DueTime = LocalDateTime.FromDateTime(request.BorrowTo); + borrowRequest.Reason = request.Reason; + + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs new file mode 100644 index 00000000..bc5c138e --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -0,0 +1,94 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetAllBorrowRequestsPaginated +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + } + } + + public record Query : IRequest> + { + public Guid? DepartmentId { get; init; } + public Guid? DocumentId { get; init; } + public Guid? EmployeeId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var borrows = _context.Borrows.AsQueryable(); + + borrows = borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(y => y.Department) + .Include(x => x.Document) + .ThenInclude(y => y.Folder) + .ThenInclude(z => z!.Locker) + .ThenInclude(t => t.Room) + .ThenInclude(s => s.Department); + + if (request.DepartmentId is not null) + { + borrows = borrows.Where(x => x.Document.Department!.Id == request.DepartmentId); + } + + if (request.EmployeeId is not null) + { + borrows = borrows.Where(x => x.Borrower.Id == request.EmployeeId); + } + + if (request.DocumentId is not null) + { + borrows = borrows.Where(x => x.Document.Id == request.DocumentId); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BorrowDto.Status); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await borrows + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs new file mode 100644 index 00000000..a645079f --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsSpecificPaginated.cs @@ -0,0 +1,79 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +[Obsolete] +public class GetAllBorrowRequestsSpecificPaginated +{ + public record Query : IRequest> + { + public Guid? DocumentId { get; init; } + public Guid? EmployeeId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var borrows = _context.Borrows.AsQueryable(); + + borrows = borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(y => y.Department) + .Include(x => x.Document) + .ThenInclude(y => y.Folder) + .ThenInclude(z => z!.Locker) + .ThenInclude(t => t.Room) + .ThenInclude(s => s.Department); + + if (request.EmployeeId is not null) + { + borrows = borrows.Where(x => x.Borrower.Id == request.EmployeeId); + } + + if (request.DocumentId is not null) + { + borrows = borrows.Where(x => x.Document.Id == request.DocumentId); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BorrowDto.Status); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await borrows + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 2272a400..a670cca7 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -23,6 +23,6 @@ public static IQueryable OrderByCustom(this IQueryable Paginate(this IQueryable items, int page, int size) { - return items.Skip((page - 1) * page).Take(size); + return items.Skip((page - 1) * size).Take(size); } } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index ae870b98..0c4aed4b 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -1,7 +1,12 @@ +using Domain.Entities; + namespace Application.Common.Interfaces; public interface ICurrentUserService { string GetRole(); string? GetDepartment(); + User GetCurrentUser(); + Guid? GetCurrentRoomForStaff(); + Guid? GetCurrentDepartmentForStaff(); } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs index bb171be9..49afc176 100644 --- a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs @@ -8,18 +8,28 @@ namespace Application.Common.Models.Dtos.Physical; public class BorrowDto : IMapFrom { public Guid Id { get; set; } - public UserDto Borrower { get; set; } - public DocumentDto Document { get; set; } + public Guid BorrowerId { get; set; } + public Guid DocumentId { get; set; } public DateTime BorrowTime { get; set; } public DateTime DueTime { get; set; } - public string Reason { get; set; } + public DateTime ActualReturnTime { get; set; } + public string Reason { get; set; } = null!; + public string Status { get; set; } = null!; public void Mapping(Profile profile) { profile.CreateMap() + .ForMember(dest => dest.BorrowerId, + opt => opt.MapFrom(src => src.Borrower.Id)) + .ForMember(dest => dest.DocumentId, + opt => opt.MapFrom(src => src.Document.Id)) + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())) .ForMember(dest => dest.BorrowTime, opt => opt.MapFrom(src => src.BorrowTime.ToDateTimeUnspecified())) .ForMember(dest => dest.DueTime, - opt => opt.MapFrom(src => src.DueTime.ToDateTimeUnspecified())); + opt => opt.MapFrom(src => src.DueTime.ToDateTimeUnspecified())) + .ForMember(dest => dest.ActualReturnTime, + opt => opt.MapFrom(src => src.ActualReturnTime.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 23f0f4ec..994b3ac8 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -1,5 +1,6 @@ using Application.Common.Mappings; using Application.Users.Queries; +using AutoMapper; using Domain.Entities.Physical; namespace Application.Common.Models.Dtos.Physical; @@ -13,4 +14,12 @@ public class DocumentDto : IMapFrom public DepartmentDto? Department { get; set; } public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } + public string Status { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())); + } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 16e9c6a7..d33d4942 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -3,6 +3,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -68,7 +69,8 @@ public async Task Handle(Command request, CancellationToken cancell DocumentType = request.DocumentType.Trim(), Importer = importer, Department = importer.Department, - Folder = folder + Folder = folder, + Status = DocumentStatus.Issued, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index d2d4f38b..de7c13cf 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -124,17 +124,17 @@ public async Task> Handle(Query request, } var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) + if (sortBy is null || !sortBy.MatchesPropertyName()) { - sortBy = nameof(LockerDto.Id); + sortBy = nameof(DocumentDto.Id); } var sortOrder = request.SortOrder ?? "asc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await documents - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 9a4b149f..f98ce633 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -23,7 +23,7 @@ public Validator() RuleFor(x => x.RoomId) .Must((query, roomId) => roomId is not null || query.LockerId is null); - } + } } public record Query : IRequest> @@ -105,8 +105,8 @@ public async Task> Handle(Query request, CancellationTo var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await folders - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 8485fffe..5e2a93ff 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -61,8 +61,8 @@ public async Task> Handle(Query request, CancellationTo var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await lockers - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index e261e6b7..31fddbb8 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -54,8 +54,8 @@ public async Task> Handle(Query request, CancellationToke var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var list = await rooms - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index c3415e00..0d78536f 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -19,13 +20,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } - + public async Task Handle(Command request, CancellationToken cancellationToken) { var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); @@ -36,16 +37,40 @@ public async Task Handle(Command request, CancellationToken cancellati var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - var staff = new Staff + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var existedStaff = await _context.Staffs + .Include(x => x.Room) + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken); + if (existedStaff is not null) { - Id = user.Id, - User = user, - Room = room - }; - - var result = await _context.Staffs.AddAsync(staff, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + if (existedStaff.Room is not null) + { + throw new ConflictException("This user is already a staff."); + } + + existedStaff.Room = room; + var result = _context.Staffs.Update(existedStaff); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + else + { + var staff = new Staff + { + Id = user.Id, + User = user, + Room = room + }; + + var result = await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } } } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index a6f90f9a..8b3076ac 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Statuses; using NodaTime; namespace Domain.Entities.Physical; @@ -9,5 +10,7 @@ public class Borrow : BaseEntity public Document Document { get; set; } = null!; public LocalDateTime BorrowTime { get; set; } public LocalDateTime DueTime { get; set; } + public LocalDateTime ActualReturnTime { get; set; } public string Reason { get; set; } = null!; + public BorrowRequestStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index 2729d8ea..81313b56 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Statuses; namespace Domain.Entities.Physical; @@ -10,4 +11,5 @@ public class Document : BaseEntity public Department? Department { get; set; } public User? Importer { get; set; } public Folder? Folder { get; set; } + public DocumentStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Statuses/BorrowRequestStatus.cs b/src/Domain/Statuses/BorrowRequestStatus.cs new file mode 100644 index 00000000..15c19a0b --- /dev/null +++ b/src/Domain/Statuses/BorrowRequestStatus.cs @@ -0,0 +1,14 @@ +namespace Domain.Statuses; + +public enum BorrowRequestStatus +{ + Approved, + Pending, + Rejected, + Overdue, + Cancelled, + CheckedOut, + Returned, + Lost, + NotProcessable, +} \ No newline at end of file diff --git a/src/Domain/Statuses/DocumentStatus.cs b/src/Domain/Statuses/DocumentStatus.cs new file mode 100644 index 00000000..133c1cd1 --- /dev/null +++ b/src/Domain/Statuses/DocumentStatus.cs @@ -0,0 +1,9 @@ +namespace Domain.Statuses; + +public enum DocumentStatus +{ + Issued, + Available, + Borrowed, + Lost, +} \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 7f879fdd..1947979e 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -185,7 +185,9 @@ public async Task RefreshTokenAsync(string token, string r public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) { - var user = _context.Users.FirstOrDefault(x => x.Email!.Equals(email)); + var user = _context.Users + .Include(x => x.Department) + .FirstOrDefault(x => x.Email!.Equals(email)); if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) { diff --git a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs index 88bd292f..78697f26 100644 --- a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs @@ -30,5 +30,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Reason) .IsRequired(); + + builder.Property(x => x.Status) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 79e66f71..4c7033c5 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -38,5 +38,8 @@ public void Configure(EntityTypeBuilder builder) .WithMany() .HasForeignKey("ImporterId") .IsRequired(false); + + builder.Property(x => x.Status) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs b/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs deleted file mode 100644 index a9fa4a85..00000000 --- a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Infrastructure.Persistence.Migrations -{ - /// - public partial class RoomMustHaveADepartment : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs similarity index 97% rename from src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs rename to src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs index a20e242e..39077769 100644 --- a/src/Infrastructure/Persistence/Migrations/00000000000009_RoomMustHaveADepartment.Designer.cs +++ b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.Designer.cs @@ -13,8 +13,8 @@ namespace Infrastructure.Persistence.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20230528182741_RoomMustHaveADepartment")] - partial class RoomMustHaveADepartment + [Migration("20230603132557_UpdateBorrow")] + partial class UpdateBorrow { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -50,6 +50,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + b.Property("BorrowTime") .HasColumnType("timestamp without time zone"); @@ -66,6 +69,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("Status") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("BorrowerId"); @@ -99,6 +105,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("Status") + .HasColumnType("integer"); + b.Property("Title") .IsRequired() .HasMaxLength(64) diff --git a/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs new file mode 100644 index 00000000..1cf4d397 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000010_UpdateBorrow.cs @@ -0,0 +1,124 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UpdateBorrow : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments"); + + migrationBuilder.DropIndex( + name: "IX_Departments_RoomId", + table: "Departments"); + + migrationBuilder.DropColumn( + name: "RoomId", + table: "Departments"); + + migrationBuilder.AlterColumn( + name: "DepartmentId", + table: "Rooms", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Documents", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ActualReturnTime", + table: "Borrows", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "Status", + table: "Borrows", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Rooms_Departments_DepartmentId", + table: "Rooms", + column: "DepartmentId", + principalTable: "Departments", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Rooms_Departments_DepartmentId", + table: "Rooms"); + + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "ActualReturnTime", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Borrows"); + + migrationBuilder.AlterColumn( + name: "DepartmentId", + table: "Rooms", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "RoomId", + table: "Departments", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Departments_RoomId", + table: "Departments", + column: "RoomId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Departments_Rooms_RoomId", + table: "Departments", + column: "RoomId", + principalTable: "Rooms", + principalColumn: "Id"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 49740741..26a343b3 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -47,6 +47,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + b.Property("BorrowTime") .HasColumnType("timestamp without time zone"); @@ -63,6 +66,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("Status") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("BorrowerId"); @@ -96,6 +102,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("Status") + .HasColumnType("integer"); + b.Property("Title") .IsRequired() .HasMaxLength(64) diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 8357655f..32feba59 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -51,7 +51,6 @@ public bool SendResetPasswordHtmlMail(string userEmail, string password) request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", json, ParameterType.RequestBody); var response = client.Execute(request); - return response.IsSuccessStatusCode; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs index 02a9e97b..cf2628d7 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Folders.Commands; +using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; using Xunit; @@ -16,36 +17,12 @@ public DisableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sajdbqwk", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = true, - Name = "A Random name", - NumberOfDocuments = 0, - Locker = locker - }; + var department = CreateDepartment(); + var folder = CreateFolder(); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); - await AddAsync(folder); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id @@ -61,6 +38,7 @@ public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] @@ -84,35 +62,12 @@ await result.Should().ThrowAsync() public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabled() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room!", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sajdbqwk!", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = false, - Name = "A Random name!", - NumberOfDocuments = 0, - Locker = locker - }; - await AddAsync(folder); + var department = CreateDepartment(); + var folder = CreateFolder(); + folder.IsAvailable = false; + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id @@ -129,50 +84,19 @@ await result.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowInvalidOperationException_WhenFolderHasDocuments() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Capacity = 24, - IsAvailable = true, - Name = "Kamito's room!!safqwf!!", - NumberOfLockers = 1, - }; - - var locker = new Locker() - { - Id = Guid.NewGuid(), - Capacity = 47, - Name = "fqwlkjdb sawfqfw wfqwfjdbqwk!", - IsAvailable = true, - Room = room, - NumberOfFolders = 1, - }; - - var folder = new Folder() - { - Id = Guid.NewGuid(), - Capacity = 12, - IsAvailable = true, - Name = "A Randasfwqfawfqom name!", - NumberOfDocuments = 1, - Locker = locker - }; - - var document = new Document() - { - DocumentType = "fqwkfwqbfk", - Id = Guid.NewGuid(), - Title = "wjqk ljfqwjlf qwkhjf ;qikwf", - Folder = folder - }; - - await AddAsync(document); + var department = CreateDepartment(); + var document = CreateNDocuments(1).First(); + var folder = CreateFolder(document); + var locker = CreateLocker(folder); + var room = CreateRoom(department, locker); + await AddAsync(room); var disableFolderCommand = new DisableFolder.Command() { FolderId = folder.Id @@ -190,5 +114,6 @@ await result.Should().ThrowAsync() Remove(folder); Remove(locker); Remove(room); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs index 2da1413f..b3ff5025 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Lockers.Commands; using Bogus; +using Domain.Entities; using Domain.Entities.Physical; using Domain.Exceptions; using FluentAssertions; @@ -18,15 +19,8 @@ public AddLockerTests(CustomApiFactory apiFactory) : base(apiFactory) public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department = CreateDepartment(); + var room = CreateRoom(department); await AddAsync(room); @@ -56,21 +50,15 @@ public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameRoom() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department = CreateDepartment(); + var room = CreateRoom(department); await AddAsync(room); @@ -93,33 +81,20 @@ public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameR // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } [Fact] public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() { // Arrange - var room1 = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department1 = CreateDepartment(); + var room1 = CreateRoom(department1); await AddAsync(room1); - var room2 = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 3, - IsAvailable = true, - NumberOfLockers = 0, - }; + var department2 = CreateDepartment(); + var room2 = CreateRoom(department2); await AddAsync(room2); @@ -160,22 +135,17 @@ public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() var room2Entity = await FindAsync(room2.Id); Remove(room1Entity); Remove(room2Entity); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); } [Fact] public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() { // Arrange - var room = new Room() - { - Id = Guid.NewGuid(), - Name = new Faker().Name.JobArea(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - IsAvailable = true, - NumberOfLockers = 0, - }; - + var department = CreateDepartment(); + var room = CreateRoom(department); + room.Capacity = 1; await AddAsync(room); var addLockerCommand = new AddLocker.Command() @@ -205,5 +175,6 @@ public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() // Cleanup var roomEntity = await FindAsync(room.Id); Remove(roomEntity); + Remove(await FindAsync(department.Id)); } } \ No newline at end of file From d66ba0ee757bffdec3af196e27a73244b0431fcd Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:12:05 +0700 Subject: [PATCH 042/162] Feat/get all staffs paginated + tests (#165) * add: implementation for staff- not done * fix: get all paginated work but wait for pagination bug to merge * feat: get all staff paginated + tests * fix: pagination order --- .../Common/Models/Dtos/Physical/StaffDto.cs | 9 +++ .../Staffs/Queries/GetAllStaffsPaginated.cs | 51 ++++++++++++ .../Queries/GetAllStaffsPaginatedTests.cs | 78 +++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs diff --git a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs index 84563c1b..cc6394f4 100644 --- a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs @@ -1,11 +1,20 @@ using Application.Common.Mappings; using Application.Users.Queries; +using AutoMapper; using Domain.Entities.Physical; namespace Application.Common.Models.Dtos.Physical; public class StaffDto : IMapFrom { + public Guid Id { get; set; } public UserDto User { get; set; } = null!; public RoomDto? Room { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Id, + opt => opt.MapFrom(src => src.User.Id)); + } } \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs index 270dfe77..8ad79883 100644 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -1,6 +1,12 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using AutoMapper.QueryableExtensions; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Staffs.Queries; @@ -14,4 +20,49 @@ public class Query : IRequest> public string? SortBy { get; set; } public string? SortOrder { get; set; } } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var staffs = _context.Staffs + .Include(x => x.Room) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + staffs = staffs.Where(x => x.User.Username.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(StaffDto.Id); + } + + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var list = await staffs + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value,sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs new file mode 100644 index 00000000..4c0547ef --- /dev/null +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs @@ -0,0 +1,78 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Application.Staffs.Queries; +using AutoMapper; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.Staffs.Queries; + +public class GetAllStaffsPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllStaffsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(config => config.AddProfile()); + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnStaffs() + { + // Arrange + var user1 = CreateUser(IdentityData.Roles.Staff, "123123"); + var user2 = CreateUser(IdentityData.Roles.Staff, "435455"); + var staff1 = CreateStaff(user1, null); + var staff2 = CreateStaff(user2, null); + await AddAsync(staff1); + await AddAsync(staff2); + + var query = new GetAllStaffsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + + // Cleanup + Remove(staff2); + Remove(staff1); + Remove(await FindAsync(user2.Id)); + Remove(await FindAsync(user1.Id)); + } + + [Fact] + public async Task ShouldReturnASpecificStaff() + { + // Arrange + var user1 = CreateUser(IdentityData.Roles.Staff, "123123"); + var user2 = CreateUser(IdentityData.Roles.Staff, "435455"); + var staff1 = CreateStaff(user1, null); + var staff2 = CreateStaff(user2, null); + await AddAsync(staff1); + await AddAsync(staff2); + + var query = new GetAllStaffsPaginated.Query() + { + SearchTerm = staff1.User.Username + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(1); + result.Items.First().Should() + .BeEquivalentTo(_mapper.Map(staff1), + config => config.Excluding(x => x.User.Created)); + + // Cleanup + Remove(staff2); + Remove(staff1); + Remove(await FindAsync(user2.Id)); + Remove(await FindAsync(user1.Id)); + } +} \ No newline at end of file From 854304d55d53ab7d9f526751144c425e8733d39b Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:57:37 +0700 Subject: [PATCH 043/162] Feat/update document (#169) * add: integration test * feat: update document * fix: test for god sake * add safety check --------- Co-authored-by: Nguyen Quang Chien --- .../Documents/Commands/UpdateDocument.cs | 72 ++++++++++++ .../Documents/Commands/UpdateDocumentTests.cs | 107 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index b0cac569..d670ed86 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -1,10 +1,34 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using FluentValidation; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Documents.Commands; public class UpdateDocument { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Title) + .NotEmpty().WithMessage("Title is required.") + .MaximumLength(64).WithMessage("Title cannot exceed 64 characters."); + + RuleFor(x => x.Description) + .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); + + RuleFor(x => x.DocumentType) + .NotEmpty().WithMessage("DocumentType is required.") + .MaximumLength(64).WithMessage("DocumentType cannot exceed 64 characters."); + } + } + public record Command : IRequest { public Guid DocumentId { get; init; } @@ -12,4 +36,52 @@ public record Command : IRequest public string? Description { get; init; } public string DocumentType { get; init; } = null!; } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include( x => x.Importer) + .FirstOrDefaultAsync( x => x.Id.Equals(request.DocumentId), cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.Importer is not null) + { + var titleExisted = await _context.Documents + .Include(x => x.Importer) + .AnyAsync(x => + x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) + && x.Id != document.Id + && x.Importer!.Id == document.Importer!.Id + , cancellationToken); + + if (titleExisted) + { + throw new ConflictException("Document name already exists for this importer."); + } + } + + document.Title = request.Title; + document.DocumentType = request.DocumentType; + document.Description = request.Description; + + var result = _context.Documents.Update(document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs new file mode 100644 index 00000000..76de4789 --- /dev/null +++ b/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs @@ -0,0 +1,107 @@ +using Application.Common.Exceptions; +using Application.Documents.Commands; +using Application.Identity; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Application.Tests.Integration.Documents.Commands; + +public class UpdateDocumentTests : BaseClassFixture +{ + public UpdateDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldUpdateDocument_WhenUpdateDetailsAreValid() + { + // Arrange + var importer = CreateUser(IdentityData.Roles.Employee, "A RANDOM PASS"); + var document = CreateNDocuments(1).First(); + document.Importer = importer; + await AddAsync(document); + + var command = new UpdateDocument.Command() + { + DocumentId = document.Id, + Title = "Khoa is ngu", + Description = "This would probably not be duplicated", + DocumentType = "Hehehe", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(command.DocumentId); + result.Title.Should().Be(command.Title); + result.Description.Should().Be(command.Description); + result.DocumentType.Should().Be(command.DocumentType); + + // Cleanup + Remove(document); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenThatDocumentDoesNotExist() + { + // Arrange + var command = new UpdateDocument.Command() + { + DocumentId = Guid.NewGuid(), + Title = "Khoa is ngu", + Description = "This would probably not be duplicated", + DocumentType = "Hehehe", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("Document does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewDocumentTitleAlreadyExistsForTheImporter() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var importer = CreateUser(IdentityData.Roles.Employee, "A RANDOM PASS"); + var documentList = CreateNDocuments(2); + + var document1 = documentList[0]; + document1.Importer = importer; + await context.AddAsync(document1); + + var document2 = documentList[1]; + document2.Importer = document1.Importer; + document2.Title = "Another title"; + await context.AddAsync(document2); + await context.SaveChangesAsync(); + + var command = new UpdateDocument.Command() + { + DocumentId = document1.Id, + Title = document2.Title, + Description = "This would probably not be duplicated", + DocumentType = "Hehehe", + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync(); + + // Clean up + Remove(document1); + Remove(document2); + Remove(importer); + } + +} \ No newline at end of file From d8601bf38d2b171c5268cb99024d63ddcba09b05 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sat, 3 Jun 2023 23:37:43 +0700 Subject: [PATCH 044/162] fix: added room checking for updating locker (#175) * fix: added room checking * fix: duplicate locker in the same room for update locker --- .../Lockers/Commands/UpdateLocker.cs | 6 ++- .../Lockers/Commands/UpdateLockerTests.cs | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index f389c468..c55e2fea 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -50,7 +50,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var locker = await _context.Lockers.FirstOrDefaultAsync( + var locker = await _context.Lockers.Include(x => x.Room).FirstOrDefaultAsync( x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) @@ -59,7 +59,9 @@ public async Task Handle(Command request, CancellationToken cancellat } var duplicateLocker = await _context.Lockers.FirstOrDefaultAsync( - x => x.Name.Equals(request.Name), cancellationToken); + x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) + && x.Id != locker.Id + && x.Room.Id == locker.Room.Id, cancellationToken); if (duplicateLocker is not null && !duplicateLocker.Equals(locker)) { diff --git a/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs index 67031617..fc390d75 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs @@ -129,4 +129,43 @@ await result.Should().ThrowAsync() Remove(room); Remove(await FindAsync(department.Id)); } + + [Fact] + public async Task ShouldUpdateLocker_WhenSameNameExistsButInDifferentRoom() + { + // Arrange + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + var locker = CreateLocker(); + var duplicateNameLocker = CreateLocker(); + var room1 = CreateRoom(department1, locker); + var room2 = CreateRoom(department2, duplicateNameLocker); + await AddAsync(room1); + await AddAsync(room2); + + var command = new UpdateLocker.Command() + { + LockerId = locker.Id, + Name = duplicateNameLocker.Name, + Capacity = 23, + Description = "fuck", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Id.Should().Be(locker.Id); + result.Name.Should().Be(command.Name); + result.Capacity.Should().Be(command.Capacity); + result.Description.Should().Be(command.Description); + + // Cleanup + Remove(locker); + Remove(duplicateNameLocker); + Remove(room1); + Remove(room2); + Remove(await FindAsync(department1.Id)); + Remove(await FindAsync(department2.Id)); + } } \ No newline at end of file From 89d5e9f56cee65498e98c801c136182336d6645f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 4 Jun 2023 00:46:12 +0700 Subject: [PATCH 045/162] Fix/logic error in update (#178) * fix: duplicate folder in the same locker for update folder * huhu --- src/Application/Folders/Commands/UpdateFolder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index 56fec760..47b6cc32 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -51,6 +51,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var folder = await _context.Folders + .Include(x => x.Locker) .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); if (folder is null) @@ -61,6 +62,7 @@ public async Task Handle(Command request, CancellationToken cancellat var nameExisted = await _context.Folders.AnyAsync( x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Id != folder.Id + && x.Locker.Id == folder.Locker.Id , cancellationToken); if (nameExisted) From 11a850e215fe2f1362d3c7a994433e6131390d29 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 4 Jun 2023 11:01:41 +0700 Subject: [PATCH 046/162] idek what to write here (#179) --- src/Api/Controllers/BorrowsController.cs | 43 +++++++++++++++ src/Api/Services/CurrentUserService.cs | 10 ++-- .../Borrows/Queries/GetBorrowRequestById.cs | 54 +++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/Application/Borrows/Queries/GetBorrowRequestById.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 0c7b34c1..394e8fb8 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -43,6 +43,28 @@ public async Task>> BorrowDocument([FromBody] Bor return Ok(Result.Succeed(result)); } + /// + /// Get a borrow request by id + /// + /// A BorrowDto of the retrieved borrow + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet("{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> GetById([FromRoute] Guid borrowId) + { + var user = _currentUserService.GetCurrentUser(); + var command = new GetBorrowRequestById.Query() + { + BorrowId = borrowId, + User = user, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + [RequiresRole(IdentityData.Roles.Staff)] [HttpGet("staffs")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -261,4 +283,25 @@ public async Task>> Update( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Cancel borrow request + /// + /// Id of the borrow request to be cancelled + /// A BorrowDto of the cancelled borrow request + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("cancel/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Cancel([FromRoute] Guid borrowId) + { + var command = new CancelBorrowRequest.Command() + { + BorrowId = borrowId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 0075bedb..920c6361 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -38,13 +38,15 @@ public string GetRole() public string? GetDepartment() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub)); + .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); } - var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + var user = _context.Users + .Include(x => x.Department) + .FirstOrDefault(x => x.Username.Equals(userName)); if (user is null) { @@ -63,7 +65,9 @@ public User GetCurrentUser() throw new UnauthorizedAccessException(); } - var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + var user = _context.Users + .Include(x => x.Department) + .FirstOrDefault(x => x.Username.Equals(userName)); if (user is null) { diff --git a/src/Application/Borrows/Queries/GetBorrowRequestById.cs b/src/Application/Borrows/Queries/GetBorrowRequestById.cs new file mode 100644 index 00000000..64486fc5 --- /dev/null +++ b/src/Application/Borrows/Queries/GetBorrowRequestById.cs @@ -0,0 +1,54 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetBorrowRequestById +{ + public record Query : IRequest + { + public Guid BorrowId { get; init; } + public User User { get; init; } = null!; + } + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var borrow = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(y => y.Department) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + + if (borrow is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (!request.User.Role.Equals(IdentityData.Roles.Employee)) + { + return _mapper.Map(borrow); + } + + return borrow.Borrower.Id != request.User.Id + ? throw new UnauthorizedAccessException() + : _mapper.Map(borrow); + } + } +} \ No newline at end of file From ac5993a2a47af3809d84fa34c022095cc46a77db Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 4 Jun 2023 11:53:16 +0700 Subject: [PATCH 047/162] huhu (#177) --- src/Api/Controllers/DocumentsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 8d817d70..2a2f369f 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -35,7 +35,6 @@ public async Task>> GetById([FromRoute] Guid do /// /// Get all documents query parameters /// A paginated list of DocumentDto - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] From 34f537f674e6965abfa7841e3691af57212234ca Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 4 Jun 2023 15:13:59 +0700 Subject: [PATCH 048/162] =?UTF-8?q?now=20can=20request=20a=20borrow=20even?= =?UTF-8?q?=20when=20a=20previous=20request=20of=20a=20user=20is=20ca?= =?UTF-8?q?=E2=80=A6=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * now can request a borrow even when a previous request of a user is cancelled * I dont have time --- src/Api/Controllers/BorrowsController.cs | 3 ++- ...questsPaginatedForDocumentQueryParameters.cs | 2 +- .../Borrows/Commands/BorrowDocument.cs | 4 +++- .../Queries/GetAllBorrowRequestsPaginated.cs | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 394e8fb8..484c3949 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -151,7 +151,7 @@ public async Task>>> GetAllRequests /// /// /// - [RequiresRole(IdentityData.Roles.Employee)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("documents/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -168,6 +168,7 @@ public async Task>>> GetAllRequests Size = queryParameters.Size, SortBy = queryParameters.SortBy, SortOrder = queryParameters.SortOrder, + Status = queryParameters.Status, }; var result = await Mediator.Send(command); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs index a44619fe..ca2c22b7 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedForDocumentQueryParameters.cs @@ -2,5 +2,5 @@ namespace Api.Controllers.Payload.Requests.Borrows; public class GetAllBorrowRequestsPaginatedForDocumentQueryParameters : PaginatedQueryParameters { - + public string? Status { get; set; } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 602003bc..ad1df6fd 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -105,7 +105,9 @@ public async Task Handle(Command request, CancellationToken cancellat { // Does not make sense if the same person go up and want to borrow the same document again // even if the borrow day will be after the due day - if (existedBorrow.Borrower.Id == request.BorrowerId) + if (existedBorrow.Borrower.Id == request.BorrowerId + && existedBorrow.Status is BorrowRequestStatus.Pending + or BorrowRequestStatus.Approved) { throw new ConflictException("This document is already requested borrow from the same user."); } diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index bc5c138e..5e30d3ec 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -4,6 +4,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Statuses; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -29,6 +30,7 @@ public record Query : IRequest> public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } + public string? Status { get; init; } } public class QueryHandler : IRequestHandler> @@ -71,6 +73,21 @@ public async Task> Handle(Query request, { borrows = borrows.Where(x => x.Document.Id == request.DocumentId); } + + if (request.Status is not null) + { + var statuses = request.Status.Split(","); + var enums = new List(); + foreach (var status in statuses) + { + if (Enum.TryParse(status, true, out BorrowRequestStatus s)) + { + enums.Add(s); + } + } + + borrows = borrows.Where(x => enums.Contains(x.Status)); + } var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) From 79121bdcb39f4e5ad14382a013a5c86f8cb7f18a Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 4 Jun 2023 17:47:19 +0700 Subject: [PATCH 049/162] Refactor/include fix (#180) * refactor: room * refactor: locker * refactor: folder * refactor: resolve requested changes * fix: im a stoopid people person thing --- src/Application/Folders/Commands/DisableFolder.cs | 3 +++ src/Application/Folders/Commands/EnableFolder.cs | 3 +++ src/Application/Folders/Commands/RemoveFolder.cs | 2 ++ src/Application/Folders/Commands/UpdateFolder.cs | 2 ++ src/Application/Folders/Queries/GetFolderById.cs | 6 +++++- src/Application/Lockers/Commands/DisableLocker.cs | 2 ++ src/Application/Lockers/Commands/EnableLocker.cs | 2 ++ src/Application/Lockers/Commands/RemoveLocker.cs | 1 + src/Application/Lockers/Commands/UpdateLocker.cs | 6 ++++-- src/Application/Lockers/Queries/GetLockerById.cs | 5 ++++- src/Application/Rooms/Commands/DisableRoom.cs | 2 ++ src/Application/Rooms/Commands/EnableRoom.cs | 2 ++ src/Application/Rooms/Commands/RemoveRoom.cs | 1 + src/Application/Rooms/Commands/UpdateRoom.cs | 2 ++ src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 1 + src/Application/Rooms/Queries/GetRoomById.cs | 2 ++ src/Application/Users/Queries/GetUserById.cs | 1 + 17 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Application/Folders/Commands/DisableFolder.cs b/src/Application/Folders/Commands/DisableFolder.cs index 020bc1fe..f57ab34b 100644 --- a/src/Application/Folders/Commands/DisableFolder.cs +++ b/src/Application/Folders/Commands/DisableFolder.cs @@ -40,6 +40,9 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(f => f.Id.Equals(request.FolderId), cancellationToken); if (folder is null) diff --git a/src/Application/Folders/Commands/EnableFolder.cs b/src/Application/Folders/Commands/EnableFolder.cs index 2cd66702..e7acf703 100644 --- a/src/Application/Folders/Commands/EnableFolder.cs +++ b/src/Application/Folders/Commands/EnableFolder.cs @@ -28,6 +28,9 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); if (folder is null) diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index 30fb5776..e8bc05f2 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -31,6 +31,8 @@ public async Task Handle(Command request, CancellationToken cancellat { var folder = await _context.Folders .Include(x => x.Locker) + .ThenInclude(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); if (folder is null) diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index 47b6cc32..edfcfe76 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -52,6 +52,8 @@ public async Task Handle(Command request, CancellationToken cancellat { var folder = await _context.Folders .Include(x => x.Locker) + .ThenInclude(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); if (folder is null) diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs index 9b1d07df..c74c30c8 100644 --- a/src/Application/Folders/Queries/GetFolderById.cs +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -26,7 +26,11 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { - var folder = await _context.Folders.FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); + var folder = await _context.Folders + .Include(x =>x .Locker) + .ThenInclude(x => x.Room) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); if (folder is null) { diff --git a/src/Application/Lockers/Commands/DisableLocker.cs b/src/Application/Lockers/Commands/DisableLocker.cs index cbde208a..10a78f79 100644 --- a/src/Application/Lockers/Commands/DisableLocker.cs +++ b/src/Application/Lockers/Commands/DisableLocker.cs @@ -40,6 +40,8 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var locker = await _context.Lockers + .Include(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) diff --git a/src/Application/Lockers/Commands/EnableLocker.cs b/src/Application/Lockers/Commands/EnableLocker.cs index 32dee1aa..49722a1a 100644 --- a/src/Application/Lockers/Commands/EnableLocker.cs +++ b/src/Application/Lockers/Commands/EnableLocker.cs @@ -40,6 +40,8 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var locker = await _context.Lockers + .Include(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) { diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index 4a0ce1fc..a6ab6ad9 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -41,6 +41,7 @@ public async Task Handle(Command request, CancellationToken cancellat { var locker = await _context.Lockers .Include(x => x.Room) + .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index c55e2fea..20383ef9 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -50,8 +50,10 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var locker = await _context.Lockers.Include(x => x.Room).FirstOrDefaultAsync( - x => x.Id.Equals(request.LockerId), cancellationToken); + var locker = await _context.Lockers + .Include(x => x.Room) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) { diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 8077335f..64a1bf5a 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -26,7 +26,10 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { - var locker = await _context.Lockers.FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); + var locker = await _context.Lockers + .Include(x => x.Room) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); if (locker is null) { diff --git a/src/Application/Rooms/Commands/DisableRoom.cs b/src/Application/Rooms/Commands/DisableRoom.cs index dd9c7afe..c70fe9a5 100644 --- a/src/Application/Rooms/Commands/DisableRoom.cs +++ b/src/Application/Rooms/Commands/DisableRoom.cs @@ -40,6 +40,8 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) .Include(x => x.Lockers) .ThenInclude(y => y.Folders) .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); diff --git a/src/Application/Rooms/Commands/EnableRoom.cs b/src/Application/Rooms/Commands/EnableRoom.cs index af5d710b..d27e9faf 100644 --- a/src/Application/Rooms/Commands/EnableRoom.cs +++ b/src/Application/Rooms/Commands/EnableRoom.cs @@ -38,6 +38,8 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 9e35ddc1..b389e8d6 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -39,6 +39,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var room = await _context.Rooms + .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index ade660fe..a4fbe702 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -50,6 +50,8 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 31fddbb8..174b8dc9 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -36,6 +36,7 @@ public async Task> Handle(Query request, CancellationToke { var rooms = _context.Rooms .Include(x => x.Department) + .Include(x => x.Staff) .AsQueryable(); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 6e834b68..2d420d10 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -28,6 +28,8 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs index c4cd1278..7b22fe51 100644 --- a/src/Application/Users/Queries/GetUserById.cs +++ b/src/Application/Users/Queries/GetUserById.cs @@ -26,6 +26,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { var user = await _context.Users + .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.UserId), cancellationToken: cancellationToken); if (user is null) From 055c47ab5db9bd40021e83167f0eae58c9055da6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 4 Jun 2023 22:40:20 +0700 Subject: [PATCH 050/162] fix: pagination working wrong (#184) --- .../Borrows/Queries/GetAllBorrowRequestsPaginated.cs | 7 ++++--- .../Documents/Queries/GetAllDocumentsPaginated.cs | 3 ++- src/Application/Folders/Queries/GetAllFoldersPaginated.cs | 3 ++- src/Application/Lockers/Queries/GetAllLockersPaginated.cs | 3 ++- src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 3 ++- src/Application/Staffs/Queries/GetAllStaffsPaginated.cs | 3 ++- src/Application/Users/Queries/GetAllUsersPaginated.cs | 3 ++- 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index 5e30d3ec..c78f674a 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -97,15 +97,16 @@ public async Task> Handle(Query request, var sortOrder = request.SortOrder ?? "asc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - + + var count = await borrows.CountAsync(cancellationToken); var list = await borrows .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); - + var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index de7c13cf..fd5c6a0b 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -132,6 +132,7 @@ public async Task> Handle(Query request, var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await documents.CountAsync(cancellationToken); var list = await documents .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value, sizeNumber.Value) @@ -139,7 +140,7 @@ public async Task> Handle(Query request, var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index f98ce633..530d039f 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -104,6 +104,7 @@ public async Task> Handle(Query request, CancellationTo var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await folders.CountAsync(cancellationToken); var list = await folders .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value, sizeNumber.Value) @@ -111,7 +112,7 @@ public async Task> Handle(Query request, CancellationTo var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 5e2a93ff..3e08e523 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -60,6 +60,7 @@ public async Task> Handle(Query request, CancellationTo var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await lockers.CountAsync(cancellationToken); var list = await lockers .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value, sizeNumber.Value) @@ -67,7 +68,7 @@ public async Task> Handle(Query request, CancellationTo var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index 174b8dc9..ff89fe2b 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -54,6 +54,7 @@ public async Task> Handle(Query request, CancellationToke var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await rooms.CountAsync(cancellationToken); var list = await rooms .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value, sizeNumber.Value) @@ -61,7 +62,7 @@ public async Task> Handle(Query request, CancellationToke var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs index 8ad79883..61f62046 100644 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -55,6 +55,7 @@ public async Task> Handle(Query request, CancellationTok var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await staffs.CountAsync(cancellationToken); var list = await staffs .OrderByCustom(sortBy, sortOrder) .Paginate(pageNumber.Value,sizeNumber.Value) @@ -62,7 +63,7 @@ public async Task> Handle(Query request, CancellationTok var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index e68b7f20..f8613324 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -59,6 +59,7 @@ public async Task> Handle(Query request, CancellationToke var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var count = await users.CountAsync(cancellationToken); var list = await users .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) @@ -66,7 +67,7 @@ public async Task> Handle(Query request, CancellationToke var result = _mapper.Map>(list); - return new PaginatedList(result, result.Count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file From 8eb386d04364be73b4884ce6ebfc9f69df17c945 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sun, 4 Jun 2023 23:04:47 +0700 Subject: [PATCH 051/162] Create versioning.yml (#185) --- .github/workflows/versioning.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/versioning.yml diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml new file mode 100644 index 00000000..cb898b0f --- /dev/null +++ b/.github/workflows/versioning.yml @@ -0,0 +1,22 @@ +name: Bump version +on: + push: + branches: + - dev + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Create a GitHub release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Release ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} From 4d735aad39d8c975ef0a89395ea6d22dd5e3e9c7 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sun, 4 Jun 2023 23:09:11 +0700 Subject: [PATCH 052/162] Update versioning.yml --- .github/workflows/versioning.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml index cb898b0f..2928e706 100644 --- a/.github/workflows/versioning.yml +++ b/.github/workflows/versioning.yml @@ -2,7 +2,6 @@ name: Bump version on: push: branches: - - dev - main jobs: build: From 402c1d79164c571645219160d37861987c7c41ec Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:38:25 +0700 Subject: [PATCH 053/162] test: create borrow request in test (#192) --- .../BaseClassFixture.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 370fbc40..ee1f2c14 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -3,6 +3,7 @@ using Domain.Common; using Domain.Entities; using Domain.Entities.Physical; +using Domain.Statuses; using Infrastructure.Persistence; using MediatR; using Microsoft.EntityFrameworkCore; @@ -196,4 +197,18 @@ protected static Staff CreateStaff(User user, Room? room) Room = room, }; } + + protected static Borrow CreateBorrowRequest(User borrower, Document document, BorrowRequestStatus status) + { + return new Borrow() + { + Id = Guid.NewGuid(), + Borrower = borrower, + Document = document, + Reason = "something something", + Status = status, + BorrowTime = LocalDateTime.FromDateTime(DateTime.UtcNow), + DueTime = LocalDateTime.FromDateTime(DateTime.UtcNow + TimeSpan.FromDays(1)) + }; + } } \ No newline at end of file From 3b43bf1633a0f86f36e29c4034fd6042ea05cd96 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:27:26 +0700 Subject: [PATCH 054/162] test: add integration tests (#209) --- .../BaseClassFixture.cs | 4 +- .../Borrows/Commands/UpdateBorrowTests.cs | 186 ++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index ee1f2c14..304ec0e7 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -207,8 +207,8 @@ protected static Borrow CreateBorrowRequest(User borrower, Document document, Bo Document = document, Reason = "something something", Status = status, - BorrowTime = LocalDateTime.FromDateTime(DateTime.UtcNow), - DueTime = LocalDateTime.FromDateTime(DateTime.UtcNow + TimeSpan.FromDays(1)) + BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), + DueTime = LocalDateTime.FromDateTime(DateTime.Now + TimeSpan.FromDays(1)) }; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs new file mode 100644 index 00000000..f00d3e25 --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs @@ -0,0 +1,186 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class UpdateBorrowTests : BaseClassFixture +{ + public UpdateBorrowTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldUpdateBorrow_WhenDetailsAreValid() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + + var document = CreateNDocuments(1).First(); + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Reason.Should().Be(command.Reason); + result.BorrowTime.Should().Be(command.BorrowFrom); + result.DueTime.Should().Be(command.BorrowTo); + + // Cleanup + Remove(await FindAsync(borrow.Id)); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() + { + // Arrange + var command = new UpdateBorrow.Command() + { + Reason = "adsda", + BorrowFrom = DateTime.Now.AddHours(1), + BorrowTo = DateTime.Now.AddHours(2), + BorrowId = Guid.NewGuid(), + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Borrow request does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPending() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Approved); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Cannot update borrow request."); + + // Cleanup + Remove(borrow); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenDocumentIsLost() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Lost; + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is lost."); + + // Cleanup + Remove(borrow); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "a"); + var user2 = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + + var borrow1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Pending); + var borrow2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Approved); + borrow2.BorrowTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(4)); + borrow2.DueTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(12)); + + await context.AddAsync(borrow1); + await context.AddAsync(borrow2); + + await context.SaveChangesAsync(); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(5), + BorrowTo = DateTime.Now.AddDays(13), + BorrowId = borrow1.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup + Remove(borrow1); + Remove(borrow2); + Remove(user1); + Remove(user2); + Remove(document); + } +} \ No newline at end of file From 6f373c97aa7b6397593a1e14d56f407650c38712 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:58:48 +0700 Subject: [PATCH 055/162] test: add integration tests for approve borrow request (#204) * test: add integration tests * fix: some stuff and add an assertion --- .../Commands/ApproveBorrowRequestTests.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs diff --git a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs new file mode 100644 index 00000000..87a40fd6 --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs @@ -0,0 +1,168 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class ApproveBorrowRequestTests : BaseClassFixture +{ + public ApproveBorrowRequestTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldApproveRequest_WhenRequestIsValid() + { + // Arrange + var document = CreateNDocuments(1).First(); + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Status.Should().Be(BorrowRequestStatus.Approved.ToString()); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() + { + // Arrange + var command = new ApproveBorrowRequest.Command() + { + BorrowId = Guid.NewGuid(), + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Borrow request does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenDocumentIsLost() + { + // Arrange + var document = CreateNDocuments(1).First(); + + document.Status = DocumentStatus.Lost; + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is lost. Request is unprocessable."); + (await FindAsync(request.Id))!.Status.Should().Be(BorrowRequestStatus.NotProcessable); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPendingAndRejected() + { + var document = CreateNDocuments(1).First(); + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.CheckedOut); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Request cannot be approved."); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() + { + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var document = CreateNDocuments(1).First(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "abcdef"); + var user2 = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + + var request1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); + + var request2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Pending); + request2.BorrowTime = request2.BorrowTime.Plus(Period.FromMinutes(30)); + request2.DueTime = request2.DueTime.Plus(Period.FromHours(1)); + + await context.AddAsync(request1); + await context.AddAsync(request2); + await context.SaveChangesAsync(); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request2.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup + Remove(request1); + Remove(request2); + Remove(user1); + Remove(user2); + Remove(document); + } +} \ No newline at end of file From 4d47762a3604d49816202f0d19eef2be25dbeec0 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:27:05 +0700 Subject: [PATCH 056/162] test: add integration tests for borrow document (#208) * test: add integration tests fix: time * fix and add stuffs * cleanup --- .../Borrows/Commands/BorrowDocumentTests.cs | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs new file mode 100644 index 00000000..b595d20f --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -0,0 +1,354 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class BorrowDocumentTests : BaseClassFixture +{ + public BorrowDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + await context.AddAsync(department); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.Department = department; + await context.AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + await context.AddAsync(document); + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + Reason = "Example", + BorrowFrom = DateTime.Now.Add(TimeSpan.FromHours(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(1)), + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.DocumentId.Should().Be(command.DocumentId); + result.BorrowerId.Should().Be(command.BorrowerId); + result.Reason.Should().Be(command.Reason); + result.BorrowTime.Should().Be(command.BorrowFrom); + result.DueTime.Should().Be(command.BorrowTo); + result.Status.Should().Be(BorrowRequestStatus.Pending.ToString()); + + // Cleanup + Remove(await FindAsync(result.Id)); + Remove(user); + Remove(document); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var command = new BorrowDocument.Command() + { + BorrowerId = Guid.NewGuid(), + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User does not exist."); + + // Cleanup + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenUserIsNotActive() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.IsActive = false; + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not active."); + + // Cleanup + Remove(document); + Remove(user); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenUserIsNotActivated() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.IsActivated = false; + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not activated."); + + // Cleanup + Remove(document); + Remove(user); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = Guid.NewGuid(), + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document does not exist."); + + // Cleanup + Remove(user); + } + + [Fact] + public async Task ShouldConflictException_WhenDocumentIsNotAvailable() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + await AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Borrowed; + await AddAsync(document); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is not available."); + + // Cleanup + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheSameDepartment() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user.Department = department1; + await context.AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department2; + await context.AddAsync(document); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not allowed to borrow this document."); + + // Cleanup + Remove(user); + Remove(document); + Remove(department1); + Remove(department2); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestWithSameUserAndDocumentAlreadyExists() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user.Department = department; + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + await context.AddAsync(borrow); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document is already requested borrow from the same user."); + + // Cleanup; + Remove(borrow); + Remove(user); + Remove(document); + Remove(department); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenARequestIsMadeWhileDocumentIsAlreadyBeingBorrowed() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user1.Department = department; + + var user2 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user2.Department = department; + await context.AddAsync(user2); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + + var borrow = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); + await context.AddAsync(borrow); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user2.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.AddHours(1), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup; + Remove(borrow); + Remove(user1); + Remove(user2); + Remove(document); + Remove(department); + } +} \ No newline at end of file From f88b11441474e557efd101394fc6731f02ac1b37 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:36:47 +0700 Subject: [PATCH 057/162] Add/schema (#202) * add: user group schema * add: digital files and entries schema * update: digital files and entries schema * add: dtos for new tables * add: base methods for entities * update: migrations indexing --- .../Interfaces/IApplicationDbContext.cs | 5 + .../Common/Models/Dtos/Digital/EntryDto.cs | 12 + .../Common/Models/Dtos/Digital/FileDto.cs | 10 + .../Models/Dtos/Digital/UserGroupDto.cs | 10 + .../Models/Dtos/Physical/DocumentDto.cs | 4 +- src/Application/Common/Models/Dtos/UserDto.cs | 3 + src/Domain/Entities/Digital/Entry.cs | 12 + src/Domain/Entities/Digital/FileEntity.cs | 9 + src/Domain/Entities/Digital/UserGroup.cs | 9 + src/Domain/Entities/Physical/Document.cs | 4 + src/Domain/Entities/User.cs | 3 + .../Persistence/ApplicationDbContext.cs | 5 + .../Configurations/DocumentConfiguration.cs | 5 + .../Configurations/EntryConfiguration.cs | 27 + .../Configurations/FileConfiguration.cs | 22 + .../Configurations/UserGroupConfiguration.cs | 25 + .../00000000000011_AddUserGroup.Designer.cs | 533 ++++++++++++++++ .../Migrations/00000000000011_AddUserGroup.cs | 67 ++ ...0000012_AddDigitalFileAndEntry.Designer.cs | 600 ++++++++++++++++++ .../00000000000012_AddDigitalFileAndEntry.cs | 94 +++ .../ApplicationDbContextModelSnapshot.cs | 114 ++++ .../BaseClassFixture.cs | 34 + .../Common/Mappings/MappingTests.cs | 5 + 23 files changed, 1611 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Models/Dtos/Digital/EntryDto.cs create mode 100644 src/Application/Common/Models/Dtos/Digital/FileDto.cs create mode 100644 src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs create mode 100644 src/Domain/Entities/Digital/Entry.cs create mode 100644 src/Domain/Entities/Digital/FileEntity.cs create mode 100644 src/Domain/Entities/Digital/UserGroup.cs create mode 100644 src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Configurations/FileConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 128372e2..8f12f9bd 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,4 +1,5 @@ using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,10 @@ public interface IApplicationDbContext public DbSet Folders { get; } public DbSet Documents { get; } public DbSet Borrows { get; } + + public DbSet UserGroups { get; } + public DbSet Files { get; } + public DbSet Entries { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs new file mode 100644 index 00000000..fe5445ab --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -0,0 +1,12 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class EntryDto : IMapFrom +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Path { get; set; } = null!; + public FileDto? File { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/FileDto.cs b/src/Application/Common/Models/Dtos/Digital/FileDto.cs new file mode 100644 index 00000000..cf2f7531 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/FileDto.cs @@ -0,0 +1,10 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class FileDto : IMapFrom +{ + public Guid Id { get; set; } + public string FileType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs new file mode 100644 index 00000000..88fb1d13 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs @@ -0,0 +1,10 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class UserGroupDto : IMapFrom +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 994b3ac8..6c517ce1 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -1,4 +1,5 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos.Digital; using Application.Users.Queries; using AutoMapper; using Domain.Entities.Physical; @@ -14,7 +15,8 @@ public class DocumentDto : IMapFrom public DepartmentDto? Department { get; set; } public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } - public string Status { get; set; } + public string Status { get; set; } = null!; + public EntryDto? Entry { get; set; } public void Mapping(Profile profile) { diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index 64cf73dc..3fe39e96 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -2,6 +2,7 @@ using Application.Common.Models.Dtos; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; namespace Application.Users.Queries; @@ -22,6 +23,8 @@ public class UserDto : IMapFrom public DateTime? LastModified { get; set; } public Guid? LastModifiedBy { get; set; } + public IEnumerable UserGroups { get; set; } + public void Mapping(Profile profile) { profile.CreateMap() diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs new file mode 100644 index 00000000..712bc0c5 --- /dev/null +++ b/src/Domain/Entities/Digital/Entry.cs @@ -0,0 +1,12 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class Entry : BaseEntity +{ + public string Name { get; set; } = null!; + public string Path { get; set; } = null!; + public Guid? FileId { get; set; } + + public virtual FileEntity? File { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/FileEntity.cs b/src/Domain/Entities/Digital/FileEntity.cs new file mode 100644 index 00000000..e965dbc7 --- /dev/null +++ b/src/Domain/Entities/Digital/FileEntity.cs @@ -0,0 +1,9 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class FileEntity : BaseEntity +{ + public string FileType { get; set; } = null!; + public byte[] FileData { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/UserGroup.cs b/src/Domain/Entities/Digital/UserGroup.cs new file mode 100644 index 00000000..23355d1e --- /dev/null +++ b/src/Domain/Entities/Digital/UserGroup.cs @@ -0,0 +1,9 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class UserGroup : BaseEntity +{ + public string Name { get; set; } = null!; + public ICollection Users { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index 81313b56..a473a2b5 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Entities.Digital; using Domain.Statuses; namespace Domain.Entities.Physical; @@ -12,4 +13,7 @@ public class Document : BaseEntity public User? Importer { get; set; } public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } + public Guid? EntryId { get; set; } + + public virtual Entry? Entry { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index cc8c2345..527300bc 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Entities.Digital; using Domain.Entities.Physical; namespace Domain.Entities; @@ -15,4 +16,6 @@ public class User : BaseAuditableEntity public string? Position { get; set; } public bool IsActive { get; set; } public bool IsActivated { get; set; } + + public ICollection UserGroups { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index dbae579c..0b8da3db 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,6 +1,7 @@ using System.Reflection; using Application.Common.Interfaces; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Infrastructure.Common; using MediatR; @@ -26,6 +27,10 @@ public ApplicationDbContext( public DbSet Folders => Set(); public DbSet Documents => Set(); public DbSet Borrows => Set(); + + public DbSet UserGroups => Set(); + public DbSet Files => Set(); + public DbSet Entries => Set(); public DbSet RefreshTokens => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 4c7033c5..a9d89058 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -41,5 +41,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Status) .IsRequired(); + + builder.HasOne(x => x.Entry) + .WithOne() + .HasForeignKey(x => x.EntryId) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs new file mode 100644 index 00000000..433d872e --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs @@ -0,0 +1,27 @@ +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class EntryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.Property(x => x.Name) + .HasMaxLength(256) + .IsRequired(); + + builder.Property(x => x.Path) + .IsRequired(); + + builder.HasOne(x => x.File) + .WithOne() + .HasForeignKey(x => x.FileId) + .IsRequired(false); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs new file mode 100644 index 00000000..d1f34b90 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs @@ -0,0 +1,22 @@ +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class FileConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.Property(x => x.FileType) + .HasMaxLength(256) + .IsRequired(); + + builder.Property(x => x.FileData) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs new file mode 100644 index 00000000..bd18e3ac --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs @@ -0,0 +1,25 @@ +using Domain.Entities; +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class UserGroupConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasMany(x => x.Users) + .WithMany(x => x.UserGroups).UsingEntity( + "Memberships", + l => l.HasOne(typeof(User)).WithMany().HasForeignKey("UserId").HasPrincipalKey(nameof(User.Id)), + r => r.HasOne(typeof(UserGroup)).WithMany().HasForeignKey("UserGroupId").HasPrincipalKey(nameof(UserGroup.Id)), + j => j.HasKey("UserId", "UserGroupId")); + + builder.HasAlternateKey(x => x.Name); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs new file mode 100644 index 00000000..132fc48c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs @@ -0,0 +1,533 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230606074719_AddUserGroup")] + partial class AddUserGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs new file mode 100644 index 00000000..db90a1c1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddUserGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserGroups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserGroups", x => x.Id); + table.UniqueConstraint("AK_UserGroups_Name", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "Memberships", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + UserGroupId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Memberships", x => new { x.UserId, x.UserGroupId }); + table.ForeignKey( + name: "FK_Memberships_UserGroups_UserGroupId", + column: x => x.UserGroupId, + principalTable: "UserGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Memberships_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Memberships_UserGroupId", + table: "Memberships", + column: "UserGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Memberships"); + + migrationBuilder.DropTable( + name: "UserGroups"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs new file mode 100644 index 00000000..63d37967 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs @@ -0,0 +1,600 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230606092912_AddDigitalFileAndEntry")] + partial class AddDigitalFileAndEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs new file mode 100644 index 00000000..ecd2d66a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs @@ -0,0 +1,94 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddDigitalFileAndEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EntryId", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FileType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + FileData = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Entries", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Path = table.Column(type: "text", nullable: false), + FileId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Entries", x => x.Id); + table.ForeignKey( + name: "FK_Entries_Files_FileId", + column: x => x.FileId, + principalTable: "Files", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Documents_EntryId", + table: "Documents", + column: "EntryId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Entries_FileId", + table: "Entries", + column: "FileId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents", + column: "EntryId", + principalTable: "Entries", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents"); + + migrationBuilder.DropTable( + name: "Entries"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropIndex( + name: "IX_Documents_EntryId", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "EntryId", + table: "Documents"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 26a343b3..deeca7c5 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -41,6 +41,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Departments"); }); + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -96,6 +159,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("EntryId") + .HasColumnType("uuid"); + b.Property("FolderId") .HasColumnType("uuid"); @@ -114,6 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("DepartmentId"); + b.HasIndex("EntryId") + .IsUnique(); + b.HasIndex("FolderId"); b.HasIndex("ImporterId"); @@ -346,6 +415,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") @@ -371,6 +464,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("DepartmentId"); + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + b.HasOne("Domain.Entities.Physical.Folder", "Folder") .WithMany("Documents") .HasForeignKey("FolderId"); @@ -381,6 +478,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); + b.Navigation("Entry"); + b.Navigation("Folder"); b.Navigation("Importer"); @@ -456,6 +555,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); }); + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Domain.Entities.Department", b => { b.Navigation("Room"); diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 304ec0e7..0339e595 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -1,7 +1,9 @@ +using System.Text; using Application.Helpers; using Bogus; using Domain.Common; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Domain.Statuses; using Infrastructure.Persistence; @@ -197,6 +199,38 @@ protected static Staff CreateStaff(User user, Room? room) Room = room, }; } + + protected static UserGroup CreateUserGroup(User[] users) + { + return new UserGroup() + { + Id = Guid.NewGuid(), + Name = new Faker().Commerce.ProductName(), + Users = users, + }; + } + + protected static FileEntity CreateFile() + { + return new FileEntity() + { + Id = Guid.NewGuid(), + FileType = new Faker().Database.Type(), + FileData = Encoding.ASCII.GetBytes(new Faker().Lorem.Random.Words()) + }; + } + + protected static Entry CreateEntry(FileEntity file) + { + return new Entry() + { + Id = Guid.NewGuid(), + Name = new Faker().Commerce.ProductName(), + File = file, + Path = new Faker().Commerce.ProductDescription(), + FileId = file.Id, + }; + } protected static Borrow CreateBorrowRequest(User borrower, Document document, BorrowRequestStatus status) { diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 28df4253..49db0eca 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -1,10 +1,12 @@ using System.Runtime.Serialization; using Application.Common.Mappings; using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Xunit; @@ -43,6 +45,9 @@ public void ShouldHaveValidConfiguration() [InlineData(typeof(Document), typeof(DocumentItemDto))] [InlineData(typeof(Borrow), typeof(BorrowDto))] [InlineData(typeof(RefreshToken), typeof(RefreshTokenDto))] + [InlineData(typeof(FileEntity), typeof(FileDto))] + [InlineData(typeof(Entry), typeof(EntryDto))] + [InlineData(typeof(UserGroup), typeof(UserGroupDto))] public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination) { // Arrange From 14d3b4a94f242bf9a80f7212feaafaafdd3d5a0f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 8 Jun 2023 08:58:25 +0700 Subject: [PATCH 058/162] Feat/reset password (#215) * add: reset password token entity, configuration, and added to app db context also renamed the parameter of OnModelCreating in ApplicationDbContext.cs to modelBuilder to satisfy SonarLint * chore: refactored SendResetPasswordHtmlMail method of IMailService.cs also: - Pass a reset password token hash so frontend can call the right link - Change one template variable name to ResetPasswordTokenHash and send the token along - Renamed HTMLMailData.cs to HtmlMailData.cs to satisfy sonarlint * add: IAuthDbContext.cs and registered it * chore: refactored IdentityService to be using abstractions instead of implementations * chore: add reset token creation logic to UserCreatedEventHandler.cs * chore: UserId foreign key naming in configuration * chore: add migrations for reset password token table * chore: modified service a little bit - Changing the way service registrations work so that 2 interfaces share the same instance - Pass the whole entity to event so that auth db context can get its id - I forgot the db context model snapshot * chore: tweaks how tokens are sent - send the token instead of the hashed version of it * add: endpoint and service to reset password --- src/Api/Controllers/AuthController.cs | 17 + .../Requests/Auth/ResetPasswordRequest.cs | 8 + .../Common/Interfaces/IAuthDbContext.cs | 12 + .../Common/Interfaces/IIdentityService.cs | 1 + .../Common/Interfaces/IMailService.cs | 2 +- .../{HTMLMailData.cs => HtmlMailData.cs} | 6 +- src/Application/Users/Commands/AddUser.cs | 2 +- .../EventHandlers/UserCreatedEventHandler.cs | 24 +- src/Domain/Entities/ResetPasswordToken.cs | 12 + src/Domain/Events/UserCreatedEvent.cs | 6 +- src/Infrastructure/ConfigureServices.cs | 3 +- .../Identity/IdentityService.cs | 74 ++- .../Persistence/ApplicationDbContext.cs | 9 +- .../ResetPasswordTokenConfiguration.cs | 24 + ...07093007_AddResetPasswordToken.Designer.cs | 520 ++++++++++++++++++ .../20230607093007_AddResetPasswordToken.cs | 48 ++ .../ApplicationDbContextModelSnapshot.cs | 34 ++ src/Infrastructure/Services/MailService.cs | 8 +- .../CustomMailService.cs | 2 +- 19 files changed, 773 insertions(+), 39 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs create mode 100644 src/Application/Common/Interfaces/IAuthDbContext.cs rename src/Application/Common/Models/{HTMLMailData.cs => HtmlMailData.cs} (86%) create mode 100644 src/Domain/Entities/ResetPasswordToken.cs create mode 100644 src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index a770a036..0190f016 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -116,6 +116,23 @@ public async Task Logout() return Ok(); } + [HttpPost("reset-password")] + public async Task ResetPassword([FromBody] ResetPasswordRequest request) + { + if (string.IsNullOrEmpty(request.NewPassword)) + { + return BadRequest("Password cannot be empty."); + } + + if (!request.NewPassword.Equals(request.ConfirmPassword)) + { + return BadRequest("Confirm password must match with new password."); + } + + await _identityService.ResetPassword(request.Token, request.NewPassword); + return Ok(); + } + private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken) { var cookieOptions = new CookieOptions diff --git a/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs b/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs new file mode 100644 index 00000000..2e18993f --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Auth; + +public class ResetPasswordRequest +{ + public string Token { get; set; } + public string NewPassword { get; set; } + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IAuthDbContext.cs b/src/Application/Common/Interfaces/IAuthDbContext.cs new file mode 100644 index 00000000..49801213 --- /dev/null +++ b/src/Application/Common/Interfaces/IAuthDbContext.cs @@ -0,0 +1,12 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Application.Common.Interfaces; + +public interface IAuthDbContext +{ + public DbSet RefreshTokens { get; } + public DbSet ResetPasswordTokens { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 048300af..c4d91102 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -9,4 +9,5 @@ public interface IIdentityService Task RefreshTokenAsync(string token, string refreshToken); Task<(AuthenticationResult AuthResult, UserDto UserCredentials)> LoginAsync(string email, string password); Task LogoutAsync(string token, string refreshToken); + Task ResetPassword(string token, string newPassword); } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs index e7fe9d67..0dbc58c7 100644 --- a/src/Application/Common/Interfaces/IMailService.cs +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -4,5 +4,5 @@ namespace Application.Common.Interfaces; public interface IMailService { - bool SendResetPasswordHtmlMail(string userEmail, string password); + bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash); } \ No newline at end of file diff --git a/src/Application/Common/Models/HTMLMailData.cs b/src/Application/Common/Models/HtmlMailData.cs similarity index 86% rename from src/Application/Common/Models/HTMLMailData.cs rename to src/Application/Common/Models/HtmlMailData.cs index edc8dae0..9fa0959b 100644 --- a/src/Application/Common/Models/HTMLMailData.cs +++ b/src/Application/Common/Models/HtmlMailData.cs @@ -2,7 +2,7 @@ namespace Application.Common.Models; -public class HTMLMailData +public class HtmlMailData { [JsonPropertyName("from")] public From From { get; set; } @@ -32,8 +32,8 @@ public class TemplateVariables { [JsonPropertyName("user_email")] public string UserEmail { get; set; } - [JsonPropertyName("pass_reset_link")] - public string PassResetLink { get; set; } + [JsonPropertyName("reset_password_token_hash")] + public string ResetPasswordTokenHash { get; set; } [JsonPropertyName("user_password")] public string UserPassword { get; set; } } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 52ca1f80..c077511c 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -106,7 +106,7 @@ public async Task Handle(Command request, CancellationToken cancellatio IsActivated = false, Created = LocalDateTime.FromDateTime(DateTime.UtcNow) }; - entity.AddDomainEvent(new UserCreatedEvent(entity.Email, password)); + entity.AddDomainEvent(new UserCreatedEvent(entity, password)); var result = await _context.Users.AddAsync(entity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs index 7c2efe9b..41b48e34 100644 --- a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -1,20 +1,40 @@ using Application.Common.Interfaces; +using Application.Helpers; +using Domain.Entities; using Domain.Events; using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Users.EventHandlers; public class UserCreatedEventHandler : INotificationHandler { private readonly IMailService _mailService; + private readonly IAuthDbContext _authDbContext; - public UserCreatedEventHandler(IMailService mailService) + public UserCreatedEventHandler(IMailService mailService, IAuthDbContext authDbContext) { _mailService = mailService; + _authDbContext = authDbContext; } public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) { - _mailService.SendResetPasswordHtmlMail(notification.Email, notification.Password); + var expirationDate = LocalDateTime.FromDateTime(DateTime.Now.AddDays(1)); + + var token = Guid.NewGuid().ToString(); + var resetPasswordToken = new ResetPasswordToken() + { + User = notification.User, + TokenHash = SecurityUtil.Hash(token), + ExpirationDate = expirationDate, + IsInvalidated = false, + }; + + await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken, cancellationToken); + await _authDbContext.SaveChangesAsync(cancellationToken); + + _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, token); } } \ No newline at end of file diff --git a/src/Domain/Entities/ResetPasswordToken.cs b/src/Domain/Entities/ResetPasswordToken.cs new file mode 100644 index 00000000..0ae14f1a --- /dev/null +++ b/src/Domain/Entities/ResetPasswordToken.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using NodaTime; + +namespace Domain.Entities; + +public class ResetPasswordToken +{ + [Key] public string TokenHash { get; set; } = null!; + public User User { get; set; } = null!; + public LocalDateTime ExpirationDate { get; set; } + public bool IsInvalidated { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Events/UserCreatedEvent.cs b/src/Domain/Events/UserCreatedEvent.cs index ee1e507e..eb43de5a 100644 --- a/src/Domain/Events/UserCreatedEvent.cs +++ b/src/Domain/Events/UserCreatedEvent.cs @@ -5,12 +5,12 @@ namespace Domain.Events; public class UserCreatedEvent : BaseEvent { - public UserCreatedEvent(string email, string password) + public UserCreatedEvent(User user, string password) { - Email = email; + User = user; Password = password; } - public string Email { get; } + public User User { get; } public string Password { get; } } \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index d3820329..3c6c8af2 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -17,7 +17,8 @@ public static class ConfigureServices public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) { services.AddApplicationDbContext(configuration); - services.AddScoped(); + services.AddScoped(sp => sp.GetService()!); + services.AddScoped(sp => sp.GetService()!); services.AddScoped(); services.AddMailService(configuration); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 1947979e..8e913ca5 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,10 +1,9 @@ -using System.Data; using System.Globalization; using System.IdentityModel.Tokens.Jwt; -using System.Reflection.Metadata.Ecma335; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography; +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; @@ -12,7 +11,6 @@ using Application.Users.Queries; using AutoMapper; using Domain.Entities; -using Infrastructure.Persistence; using Infrastructure.Shared; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -26,16 +24,25 @@ public class IdentityService : IIdentityService { private readonly TokenValidationParameters _tokenValidationParameters; private readonly JweSettings _jweSettings; - private readonly ApplicationDbContext _context; + private readonly IApplicationDbContext _applicationDbContext; + private readonly IAuthDbContext _authDbContext; private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; - public IdentityService(TokenValidationParameters tokenValidationParameters, IOptions jweSettingsOptions, ApplicationDbContext context, RSA encryptionKey, ECDsa signingKey, IMapper mapper) + public IdentityService( + TokenValidationParameters tokenValidationParameters, + IOptions jweSettingsOptions, + IApplicationDbContext applicationDbContext, + IAuthDbContext authDbContext, + RSA encryptionKey, + ECDsa signingKey, + IMapper mapper) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; - _context = context; + _applicationDbContext = applicationDbContext; + _authDbContext = authDbContext; _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; @@ -54,7 +61,7 @@ public async Task Validate(string token, string refreshToken) var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; - var user = await _context.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -75,7 +82,7 @@ public async Task Validate(string token, string refreshToken) } var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; - var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + var storedRefreshToken = await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) { @@ -113,7 +120,7 @@ public async Task RefreshTokenAsync(string token, string r var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; - var user = await _context.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -123,7 +130,7 @@ public async Task RefreshTokenAsync(string token, string r } var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; - var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + var storedRefreshToken = await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) { @@ -185,7 +192,7 @@ public async Task RefreshTokenAsync(string token, string r public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) { - var user = _context.Users + var user = _applicationDbContext.Users .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); @@ -194,10 +201,10 @@ public async Task RefreshTokenAsync(string token, string r throw new AuthenticationException("Username or password is invalid."); } - var existedRefreshTokens = _context.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); + var existedRefreshTokens = _authDbContext.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); - _context.RemoveRange(existedRefreshTokens); - await _context.SaveChangesAsync(); + _authDbContext.RefreshTokens.RemoveRange(existedRefreshTokens); + await _authDbContext.SaveChangesAsync(CancellationToken.None); return (await GenerateAuthenticationResultForUserAsync(user), _mapper.Map(user)); } @@ -213,7 +220,7 @@ public async Task LogoutAsync(string token, string refreshToken) var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; var storedRefreshToken = - await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) return; @@ -222,8 +229,37 @@ public async Task LogoutAsync(string token, string refreshToken) throw new AuthenticationException("This refresh token does not match this Jwt."); } - _context.RefreshTokens.Remove(storedRefreshToken); - await _context.SaveChangesAsync(); + _authDbContext.RefreshTokens.Remove(storedRefreshToken); + await _authDbContext.SaveChangesAsync(CancellationToken.None); + } + + public async Task ResetPassword(string token, string newPassword) + { + var tokenHash = SecurityUtil.Hash(token); + var resetPasswordToken = await _authDbContext.ResetPasswordTokens + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.TokenHash.Equals(tokenHash)); + if (resetPasswordToken is null) + { + throw new KeyNotFoundException("Token is invalid."); + } + + if (resetPasswordToken.IsInvalidated) + { + throw new ConflictException("Token is invalid."); + } + + var user = resetPasswordToken.User; + + if (user.IsActivated is false) + { + user.IsActivated = true; + } + + user.PasswordHash = SecurityUtil.Hash(newPassword); + resetPasswordToken.IsInvalidated = true; + await _applicationDbContext.SaveChangesAsync(CancellationToken.None); + await _authDbContext.SaveChangesAsync(CancellationToken.None); } private async Task GenerateAuthenticationResultForUserAsync(User user) @@ -239,8 +275,8 @@ private async Task GenerateAuthenticationResultForUserAsyn ExpiryDateTime = LocalDateTime.FromDateTime(utcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)) }; - await _context.RefreshTokens.AddAsync(refreshToken); - await _context.SaveChangesAsync(); + await _authDbContext.RefreshTokens.AddAsync(refreshToken); + await _authDbContext.SaveChangesAsync(CancellationToken.None); return new() { Token = token, diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 0b8da3db..91c14162 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -9,7 +9,7 @@ namespace Infrastructure.Persistence; -public class ApplicationDbContext : DbContext, IApplicationDbContext +public class ApplicationDbContext : DbContext, IApplicationDbContext, IAuthDbContext { private readonly IMediator _mediator; public ApplicationDbContext( @@ -33,13 +33,14 @@ public ApplicationDbContext( public DbSet Entries => Set(); public DbSet RefreshTokens => Set(); + public DbSet ResetPasswordTokens => Set(); - protected override void OnModelCreating(ModelBuilder builder) + protected override void OnModelCreating(ModelBuilder modelBuilder) { // Scan for entity configurations using FluentAPI - builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - base.OnModelCreating(builder); + base.OnModelCreating(modelBuilder); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) diff --git a/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs new file mode 100644 index 00000000..36b168e6 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs @@ -0,0 +1,24 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class ResetPasswordTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.TokenHash); + + builder.Property(x => x.ExpirationDate) + .IsRequired(); + + builder.Property(x => x.IsInvalidated) + .HasDefaultValue(false); + + builder.HasOne(x => x.User) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs new file mode 100644 index 00000000..22ae64be --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs @@ -0,0 +1,520 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230607093007_AddResetPasswordToken")] + partial class AddResetPasswordToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs new file mode 100644 index 00000000..3a34e1da --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddResetPasswordToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ResetPasswordTokens", + columns: table => new + { + TokenHash = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ExpirationDate = table.Column(type: "timestamp without time zone", nullable: false), + IsInvalidated = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResetPasswordTokens", x => x.TokenHash); + table.ForeignKey( + name: "FK_ResetPasswordTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ResetPasswordTokens_UserId", + table: "ResetPasswordTokens", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ResetPasswordTokens"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index deeca7c5..d7177ccb 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -349,6 +349,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens"); }); + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.Property("Id") @@ -546,6 +569,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.HasOne("Domain.Entities.Department", "Department") diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 32feba59..8515cad5 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -17,9 +17,9 @@ public MailService(IOptions mailSettingsOptions) _mailSettings = mailSettingsOptions.Value; } - public bool SendResetPasswordHtmlMail(string userEmail, string password) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) { - var data = new HTMLMailData() + var data = new HtmlMailData() { From = new From() { @@ -34,8 +34,8 @@ public bool SendResetPasswordHtmlMail(string userEmail, string password) TemplateVariables = new TemplateVariables() { UserEmail = userEmail, - PassResetLink = "random_shit", - UserPassword = password, + ResetPasswordTokenHash = resetPasswordTokenHash, + UserPassword = temporaryPassword, }, }; diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs index e439bf07..32c8d360 100644 --- a/tests/Application.Tests.Integration/CustomMailService.cs +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -4,7 +4,7 @@ namespace Application.Tests.Integration; public class CustomMailService : IMailService { - public bool SendResetPasswordHtmlMail(string userEmail, string password) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) { return true; } From 160cdbde9c0b992d29c835fb3bd5eb2a81b271ad Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 10 Jun 2023 20:34:49 +0700 Subject: [PATCH 059/162] refactor: add salt (#224) * test: add unit test for password hashing * feat(SecurityUtil.cs): add password hashing implementation * refactor: add salt and pepper for password hashing * fix(IdentityService.cs): remove unused dependencies --- src/Api/Controllers/AuthController.cs | 3 +- src/Api/appsettings.Development.json | 4 + src/Api/appsettings.Testing.json | 3 + .../Common/Interfaces/ISecurityService.cs | 6 + src/Application/Helpers/SecurityUtil.cs | 7 + src/Application/Helpers/StringUtil.cs | 4 + src/Application/Users/Commands/AddUser.cs | 11 +- src/Domain/Entities/User.cs | 1 + src/Infrastructure/ConfigureServices.cs | 15 + .../Identity/IdentityService.cs | 12 +- .../Persistence/ApplicationDbContextSeed.cs | 12 +- .../Configurations/UserConfiguration.cs | 4 + ...13_AddPasswordSaltFieldToUsers.Designer.cs | 491 ++++++++++++++++++ ...00000000013_AddPasswordSaltFieldToUsers.cs | 30 ++ .../ApplicationDbContextModelSnapshot.cs | 5 + .../Services/SecurityService.cs | 21 + src/Infrastructure/Shared/SecuritySettings.cs | 6 + .../BaseClassFixture.cs | 5 +- .../Helpers/SecurityUtilTests.cs | 27 + 19 files changed, 653 insertions(+), 14 deletions(-) create mode 100644 src/Application/Common/Interfaces/ISecurityService.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs create mode 100644 src/Infrastructure/Services/SecurityService.cs create mode 100644 src/Infrastructure/Shared/SecuritySettings.cs create mode 100644 tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 0190f016..2f8f4e4f 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -115,8 +115,7 @@ public async Task Logout() return Ok(); } - - [HttpPost("reset-password")] + public async Task ResetPassword([FromBody] ResetPasswordRequest request) { if (string.IsNullOrEmpty(request.NewPassword)) diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index ef1296b0..c0380e8b 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -9,6 +9,10 @@ "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, + "SecuritySettings": { + "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53" + }, + "MailSettings": { "ClientUrl": "https://send.api.mailtrap.io/api/send", "Token": "745f040659edff0ce87b545567da72d2", diff --git a/src/Api/appsettings.Testing.json b/src/Api/appsettings.Testing.json index 5dbdc362..aafebb14 100644 --- a/src/Api/appsettings.Testing.json +++ b/src/Api/appsettings.Testing.json @@ -5,6 +5,9 @@ "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, + "SecuritySettings": { + "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53" + }, "Seed": true, "Serilog" : { "MinimumLevel" : { diff --git a/src/Application/Common/Interfaces/ISecurityService.cs b/src/Application/Common/Interfaces/ISecurityService.cs new file mode 100644 index 00000000..5cbd6f96 --- /dev/null +++ b/src/Application/Common/Interfaces/ISecurityService.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Interfaces; + +public interface ISecurityService +{ + string Hash(string input, string salt); +} \ No newline at end of file diff --git a/src/Application/Helpers/SecurityUtil.cs b/src/Application/Helpers/SecurityUtil.cs index 1ea689af..9936b212 100644 --- a/src/Application/Helpers/SecurityUtil.cs +++ b/src/Application/Helpers/SecurityUtil.cs @@ -19,4 +19,11 @@ public static string Hash(string input) return stringBuilder.ToString(); } + + public static string HashPasswordWith(this string input, string salt, string pepper) + { + pepper = Convert.ToBase64String(Encoding.UTF8.GetBytes(pepper)); + salt = Convert.ToBase64String(Encoding.UTF8.GetBytes(salt)); + return Hash(salt + input + pepper); + } } \ No newline at end of file diff --git a/src/Application/Helpers/StringUtil.cs b/src/Application/Helpers/StringUtil.cs index 09f68c24..ec3c3b74 100644 --- a/src/Application/Helpers/StringUtil.cs +++ b/src/Application/Helpers/StringUtil.cs @@ -16,4 +16,8 @@ public static string RandomString(int n) return stringBuilder.ToString(); } + + public static string RandomPassword() => RandomString(8); + + public static string RandomSalt() => RandomString(24); } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index c077511c..2ccef831 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -66,11 +66,13 @@ public class AddUserCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly ISecurityService _securityService; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper) + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService) { _context = context; _mapper = mapper; + _securityService = securityService; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -91,11 +93,14 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Department does not exist."); } - var password = StringUtil.RandomString(8); + var password = StringUtil.RandomPassword(); + var salt = StringUtil.RandomSalt(); + var entity = new User { Username = request.Username, - PasswordHash = SecurityUtil.Hash(password), + PasswordHash = _securityService.Hash(password, salt), + PasswordSalt = salt, Email = request.Email, FirstName = request.FirstName?.Trim(), LastName = request.LastName?.Trim(), diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index 527300bc..014d974e 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -9,6 +9,7 @@ public class User : BaseAuditableEntity public string Username { get; set; } = null!; public string Email { get; set; } = null!; public string PasswordHash { get; set; } = null!; + public string PasswordSalt { get; set; } = null!; public string? FirstName { get; set; } public string? LastName { get; set; } public Department? Department { get; set; } diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 3c6c8af2..3d4ef3bb 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddJweAuthentication(configuration); services.AddAuthorization(); + services.AddSecurityService(configuration); return services; } @@ -109,4 +110,18 @@ private static IServiceCollection AddMailService(this IServiceCollection service return services; } + + private static IServiceCollection AddSecurityService(this IServiceCollection services, IConfiguration configuration) + { + var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); + + services.Configure(option => + { + option.Pepper = securitySettings!.Pepper; + }); + + services.AddTransient(); + + return services; + } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 8e913ca5..ef335a22 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -29,6 +29,7 @@ public class IdentityService : IIdentityService private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; + private readonly SecuritySettings _securitySettings; public IdentityService( TokenValidationParameters tokenValidationParameters, @@ -37,7 +38,8 @@ public IdentityService( IAuthDbContext authDbContext, RSA encryptionKey, ECDsa signingKey, - IMapper mapper) + IMapper mapper, + IOptions securitySettingsOptions) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; @@ -46,6 +48,7 @@ public IdentityService( _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; + _securitySettings = securitySettingsOptions.Value; } public async Task Validate(string token, string refreshToken) @@ -196,7 +199,7 @@ public async Task RefreshTokenAsync(string token, string r .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); - if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) + if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) { throw new AuthenticationException("Username or password is invalid."); } @@ -255,8 +258,9 @@ public async Task ResetPassword(string token, string newPassword) { user.IsActivated = true; } - - user.PasswordHash = SecurityUtil.Hash(newPassword); + var salt = StringUtil.RandomSalt(); + user.PasswordSalt = salt; + user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index a6593d43..37a37324 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -14,10 +14,11 @@ public class ApplicationDbContextSeed public static async Task Seed(ApplicationDbContext context, IConfiguration configuration, ILogger logger) { if (!configuration.GetValue("Seed")) return; - + + var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); try { - await TrySeedAsync(context); + await TrySeedAsync(context, securitySettings!.Pepper); } catch (Exception ex) { @@ -26,19 +27,22 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi } } - private static async Task TrySeedAsync(ApplicationDbContext context) + private static async Task TrySeedAsync(ApplicationDbContext context, string pepper) { var department = new Department() { Name = "Admin" }; + + var salt = StringUtil.RandomSalt(); // Default users var admin = new User { Username = "admin", Email = "admin@profile.dev", - PasswordHash = SecurityUtil.Hash("admin"), + PasswordHash = "admin".HashPasswordWith(salt, pepper), + PasswordSalt = salt, IsActive = true, IsActivated = true, Created = LocalDateTime.FromDateTime(DateTime.UtcNow), diff --git a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs index b3e55685..3240464d 100644 --- a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -24,6 +24,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.PasswordHash) .HasMaxLength(64) .IsRequired(); + + builder.Property(x => x.PasswordSalt) + .HasMaxLength(32) + .IsRequired(); builder.Property(x => x.FirstName) .HasMaxLength(50) diff --git a/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs new file mode 100644 index 00000000..d702cff5 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs @@ -0,0 +1,491 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230608044119_AddPasswordSaltFieldToUsers")] + partial class AddPasswordSaltFieldToUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs new file mode 100644 index 00000000..6b4e0f69 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddPasswordSaltFieldToUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordSalt", + table: "Users", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordSalt", + table: "Users"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index d7177ccb..9fab7c8c 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -417,6 +417,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Position") .HasMaxLength(64) .HasColumnType("character varying(64)"); diff --git a/src/Infrastructure/Services/SecurityService.cs b/src/Infrastructure/Services/SecurityService.cs new file mode 100644 index 00000000..9e38f89e --- /dev/null +++ b/src/Infrastructure/Services/SecurityService.cs @@ -0,0 +1,21 @@ +using Application.Common.Interfaces; +using Application.Helpers; +using Infrastructure.Shared; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Services; + +public class SecurityService : ISecurityService +{ + private readonly SecuritySettings _securitySettings; + + public SecurityService(IOptions securitySettingsOptions) + { + _securitySettings = securitySettingsOptions.Value; + } + + public string Hash(string input, string salt) + { + return input.HashPasswordWith(salt, _securitySettings.Pepper); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Shared/SecuritySettings.cs b/src/Infrastructure/Shared/SecuritySettings.cs new file mode 100644 index 00000000..beba3ec4 --- /dev/null +++ b/src/Infrastructure/Shared/SecuritySettings.cs @@ -0,0 +1,6 @@ +namespace Infrastructure.Shared; + +public class SecuritySettings +{ + public string Pepper { get; set; } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 0339e595..eaa0afa5 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -174,6 +174,8 @@ protected static Department CreateDepartment() protected static User CreateUser(string role, string password) { + var salt = StringUtil.RandomSalt(); + return new User() { Id = Guid.NewGuid(), @@ -186,7 +188,8 @@ protected static User CreateUser(string role, string password) IsActivated = true, IsActive = true, Created = LocalDateTime.FromDateTime(DateTime.Now), - PasswordHash = SecurityUtil.Hash(password) + PasswordHash = password.HashPasswordWith(salt, "random pepper"), + PasswordSalt = salt }; } diff --git a/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs b/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs new file mode 100644 index 00000000..259638f7 --- /dev/null +++ b/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs @@ -0,0 +1,27 @@ +using Application.Helpers; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Unit.Helpers; + +public class SecurityUtilTests +{ + public SecurityUtilTests() + { + } + + [Fact] + public void ShouldReturnHash_WhenHashPasswordWithSaltAndPepper() + { + // Arrange + string salt = "dwnqjkdqwW4q"; + string input = "ThizIsAveRyl0000GandS3cur4dP@ssWord"; + string pepper = "Some secret here"; + string expectedHash = "27745bdd5e09aae12213a1ea7a3f9056b7de257050370d6cfa1c2d1b954de335"; + // Act + string password = input.HashPasswordWith(salt, pepper); + + // Assert + password.Should().Be(expectedHash); + } +} \ No newline at end of file From 295d2b99bb9aed754f49cdfaa03237aa16b37e90 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Mon, 12 Jun 2023 18:05:27 +0700 Subject: [PATCH 060/162] fix: seed some more data and fix swagger not loading --- src/Api/Controllers/AuthController.cs | 1 + .../Persistence/ApplicationDbContextSeed.cs | 60 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 2f8f4e4f..7d11f4f2 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -116,6 +116,7 @@ public async Task Logout() return Ok(); } + [HttpPost] public async Task ResetPassword([FromBody] ResetPasswordRequest request) { if (string.IsNullOrEmpty(request.NewPassword)) diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index 37a37324..f788d804 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -3,7 +3,6 @@ using Domain.Entities; using Infrastructure.Shared; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; using NodaTime; using Serilog; @@ -49,6 +48,36 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp Role = IdentityData.Roles.Admin, }; + salt = StringUtil.RandomSalt(); + var staff = new User() + { + Username = "staff", + Email = "staff@profile.dev", + PasswordHash = "staff".HashPasswordWith(salt, pepper), + PasswordSalt = salt, + IsActive = true, + IsActivated = true, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Role = IdentityData.Roles.Staff, + }; + + var itDepartment = new Department() + { + Name = "IT" + }; + salt = StringUtil.RandomSalt(); + var employee = new User() + { + Username = "employee", + Email = "employee@profile.dev", + PasswordHash = "employee".HashPasswordWith(salt, pepper), + PasswordSalt = salt, + IsActive = true, + IsActivated = true, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Role = IdentityData.Roles.Employee, + }; + if (context.Departments.All(u => u.Name != department.Name)) { await context.Departments.AddAsync(department); @@ -57,6 +86,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = department; await context.Users.AddAsync(admin); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = department; + await context.Users.AddAsync(staff); + } } else { @@ -66,6 +100,30 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = departmentEntity; await context.Users.AddAsync(admin); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = departmentEntity; + await context.Users.AddAsync(staff); + } + } + + if (context.Departments.All(u => u.Name != itDepartment.Name)) + { + await context.Departments.AddAsync(itDepartment); + if (context.Users.All(u => u.Username != employee.Username)) + { + employee.Department = itDepartment; + await context.Users.AddAsync(employee); + } + } + else + { + var departmentEntity = context.Departments.Single(x => x.Name.Equals(itDepartment.Name)); + if (context.Users.All(u => u.Username != employee.Username)) + { + employee.Department = departmentEntity; + await context.Users.AddAsync(employee); + } } await context.SaveChangesAsync(); From b15d82142f42e7615714e410b756dc1706bf6f64 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Wed, 21 Jun 2023 21:05:44 +0700 Subject: [PATCH 061/162] Update/borrow (#261) * add: user group schema * add: digital files and entries schema * update: digital files and entries schema * add: dtos for new tables * add: base methods for entities * update: migrations indexing * test: add integration tests (#209) * test: add integration tests for approve borrow request (#204) * test: add integration tests * fix: some stuff and add an assertion * test: add integration tests for borrow document (#208) * test: add integration tests fix: time * fix and add stuffs * cleanup * Feat/reset password (#215) * add: reset password token entity, configuration, and added to app db context also renamed the parameter of OnModelCreating in ApplicationDbContext.cs to modelBuilder to satisfy SonarLint * chore: refactored SendResetPasswordHtmlMail method of IMailService.cs also: - Pass a reset password token hash so frontend can call the right link - Change one template variable name to ResetPasswordTokenHash and send the token along - Renamed HTMLMailData.cs to HtmlMailData.cs to satisfy sonarlint * add: IAuthDbContext.cs and registered it * chore: refactored IdentityService to be using abstractions instead of implementations * chore: add reset token creation logic to UserCreatedEventHandler.cs * chore: UserId foreign key naming in configuration * chore: add migrations for reset password token table * chore: modified service a little bit - Changing the way service registrations work so that 2 interfaces share the same instance - Pass the whole entity to event so that auth db context can get its id - I forgot the db context model snapshot * chore: tweaks how tokens are sent - send the token instead of the hashed version of it * add: endpoint and service to reset password * refactor: add salt (#224) * test: add unit test for password hashing * feat(SecurityUtil.cs): add password hashing implementation * refactor: add salt and pepper for password hashing * fix(IdentityService.cs): remove unused dependencies * fix: seed some more data and fix swagger not loading * add: logging entities * add: permission entity * add: request logs * add: document now has IsPrivate property and user log * add: current user service and permission manager * update: clear mapping for jwt claims * update: fix login * fix: something * add: logging to room, locker, folder creation and update * add: logging to users and staffs * update: restore import endpoint * Update/import (#239) * update: add audit and private status to document when create new import request * add: get all issued documents * add: checkin endpoint * add: approve and reject endpoint for document * finish: almost everything * reason * rename endpoint * a * modified: get all document now only return public document (#236) * get document log by id + get document logs paginated (#244) * permission * feat: get user log by id + get user logs paginated (#247) Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> * feat: get folder log by id + get folder logs paginated (#251) * feat: get folder log by id + get folder logs paginated * fix: my skill issue --------- Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> * Feat: implement get locker logs paginated + get locker log by id (#256) * feat: implement getting all logs and by Id for lockers * refactoring * refactoring * Update GetAllLockerLogsPaginated.cs * feat: implement get employees for the department the current user is in (#235) * feat: implement get employees for the department the current user is in * refactoring * Update GetAllEmployeesPaginated.cs * refactoring --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> * feat: get room log by ID and get room logs paginated (#241) * feat: get room log by ID and logs paginated * add: documentation for endpoints --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> * feat: implement get documents of a user (#240) * feat: implement get documents of a user * refactoring * feat: implement get request logs paginated + get request log by id (#257) * feat: implement get borrow request logs paginated + get borrow request log by id * added borrow type filter * refactoring * Update GetAllRequestLogsPaginated.cs * feat: get self documents (#238) * feat: get self documents * fix: minor stuff --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> * adsfads * fuck me * fuck me * fuck me 2 * fuck me 3 * fuck me 4 * haha * fuck me * i dont know what i do * fuck me 5 * borrow works now * refactor kinda oke i guess * fuck * add: migration for Logging * fix: migrations work now * test: borrow is end to end test ok I think, suppose, nothing gonna go wrong rite? * test: import request are done * final * Delete .fleet directory * final 2 --------- Co-authored-by: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Co-authored-by: kaitozu <43519768+kaitoz11@users.noreply.github.com> Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> Co-authored-by: Vzart --- src/Api/ConfigureServices.cs | 1 + src/Api/Controllers/BorrowsController.cs | 182 +-- src/Api/Controllers/DepartmentsController.cs | 72 +- src/Api/Controllers/DocumentsController.cs | 179 ++- src/Api/Controllers/FoldersController.cs | 110 +- .../Controllers/ImportRequestsController.cs | 187 +++ src/Api/Controllers/LockersController.cs | 109 +- .../ApproveOrRejectBorrowRequestRequest.cs | 7 + ...BorrowRequestsPaginatedQueryParameters.cs} | 6 +- .../Payload/Requests/Borrows/RejectRequest.cs | 6 + .../Documents/ApproveOrRejectImportRequest.cs | 7 + .../AssignDocumentToFolderRequest.cs | 6 + ...entsForEmployeePaginatedQueryParameters.cs | 9 + ...cumentsForStaffPaginatedQueryParameters.cs | 17 + ...GetAllDocumentsPaginatedQueryParameters.cs | 4 + .../GetAllIssuedPaginatedQueryParameters.cs | 6 + ...DocumentsOfUserPaginatedQueryParameters.cs | 6 + ...etSelfDocumentsPaginatedQueryParameters.cs | 9 + .../Requests/Documents/RejectImportRequest.cs | 6 + .../Documents/RequestImportDocumentRequest.cs | 24 + .../Documents/SharePermissionsRequest.cs | 9 + .../Documents/UpdateDocumentRequest.cs | 1 + .../GetAllLogsPaginatedQueryParameters.cs | 25 + ...lImportRequestsPaginatedQueryParameters.cs | 10 + .../Requests/Lockers/AddLockerRequest.cs | 8 +- .../GetAllRoomsPaginatedQueryParameters.cs | 1 + .../Requests/Rooms/UpdateRoomRequest.cs | 4 + .../Requests/Staffs/AddStaffRequest.cs | 2 +- ...GetAllEmployeesPaginatedQueryParameters.cs | 9 + .../GetAllUsersPaginatedQueryParameters.cs | 3 +- .../Requests/Users/UpdateSelfRequest.cs | 16 + .../Requests/Users/UpdateUserRequest.cs | 2 + src/Api/Controllers/RoomsController.cs | 133 +- src/Api/Controllers/StaffsController.cs | 71 +- src/Api/Controllers/UsersController.cs | 135 +- src/Api/Middlewares/ExceptionMiddleware.cs | 2 +- src/Api/Services/CurrentUserService.cs | 81 +- src/Api/Services/ExpiryPermissionService.cs | 30 + src/Application/Application.csproj | 1 + .../Borrows/Commands/ApproveBorrowRequest.cs | 80 -- .../Commands/ApproveOrRejectBorrowRequest.cs | 156 +++ .../Borrows/Commands/BorrowDocument.cs | 75 +- .../Borrows/Commands/CancelBorrowRequest.cs | 31 +- .../Borrows/Commands/CheckoutDocument.cs | 44 +- .../Borrows/Commands/RejectBorrowRequest.cs | 50 - .../Borrows/Commands/ReturnDocument.cs | 46 +- .../Borrows/Commands/UpdateBorrow.cs | 61 +- .../Queries/GetAllBorrowRequestsPaginated.cs | 63 +- .../Queries/GetAllRequestLogsPaginated.cs | 58 + .../Common/Extensions/QueryableExtensions.cs | 65 +- .../Common/Extensions/StringExtensions.cs | 18 + .../Interfaces/IApplicationDbContext.cs | 10 + .../Common/Interfaces/ICurrentUserService.cs | 3 +- .../Common/Interfaces/IDateTimeProvider.cs | 6 + .../Common/Interfaces/IPermissionManager.cs | 12 + .../Common/Messages/DocumentLogMessages.cs | 30 + .../Common/Messages/FolderLogMessage.cs | 9 + .../Common/Messages/LockerLogMessage.cs | 8 + .../Common/Messages/RequestLogMessages.cs | 10 + .../Common/Messages/RoomLogMessage.cs | 8 + .../Common/Messages/UserLogMessages.cs | 16 + src/Application/Common/Models/Dtos/BaseDto.cs | 6 + .../Common/Models/Dtos/DepartmentDto.cs | 12 +- .../Common/Models/Dtos/Digital/EntryDto.cs | 3 +- .../Common/Models/Dtos/Digital/FileDto.cs | 3 +- .../Models/Dtos/Digital/UserGroupDto.cs | 3 +- .../Dtos/ImportDocument/ImportRequestDto.cs | 22 + .../Dtos/ImportDocument/IssuedDocumentDto.cs | 24 + .../ImportDocument/IssuedRequestRoomDto.cs | 21 + .../Models/Dtos/ImportDocument/IssuerDto.cs | 16 + .../Models/Dtos/Logging/DocumentLogDto.cs | 25 + .../Models/Dtos/Logging/FolderLogDto.cs | 22 + .../Models/Dtos/Logging/LockerLogDto.cs | 22 + .../Models/Dtos/Logging/RequestLogDto.cs | 25 + .../Common/Models/Dtos/Logging/RoomLogDto.cs | 25 + .../Common/Models/Dtos/Logging/UserLogDto.cs | 22 + .../Common/Models/Dtos/Physical/BorrowDto.cs | 6 +- .../Models/Dtos/Physical/DocumentDto.cs | 4 +- .../Models/Dtos/Physical/DocumentItemDto.cs | 3 +- .../Models/Dtos/Physical/EmptyFolderDto.cs | 2 +- .../Models/Dtos/Physical/EmptyLockerDto.cs | 3 +- .../Common/Models/Dtos/Physical/FolderDto.cs | 3 +- .../Common/Models/Dtos/Physical/LockerDto.cs | 3 +- .../Models/Dtos/Physical/PermissionDto.cs | 24 + .../Common/Models/Dtos/Physical/RoomDto.cs | 3 +- .../Common/Models/Dtos/Physical/StaffDto.cs | 3 +- src/Application/Common/Models/Dtos/UserDto.cs | 3 +- .../Models/Operations/DocumentOperation.cs | 7 + .../Departments/Commands/AddDepartment.cs | 2 +- .../Departments/Commands/UpdateDepartment.cs | 14 - .../Departments/Queries/GetDepartmentById.cs | 9 + .../Documents/Commands/DeleteDocument.cs | 30 +- .../Documents/Commands/ImportDocument.cs | 50 +- .../Documents/Commands/ShareDocument.cs | 152 +++ .../Documents/Commands/UpdateDocument.cs | 43 +- .../Queries/GetAllDocumentLogsPaginated.cs | 62 + .../GetAllDocumentsForEmployeePaginated.cs | 102 ++ .../Queries/GetAllDocumentsPaginated.cs | 77 +- .../Queries/GetAllIssuedDocumentsPaginated.cs | 64 + .../Documents/Queries/GetDocumentById.cs | 42 +- .../Queries/GetDocumentsOfUserPaginated.cs | 61 + .../Documents/Queries/GetPermissions.cs | 84 ++ .../Queries/GetSelfDocumentsPaginated.cs | 59 + src/Application/Folders/Commands/AddFolder.cs | 55 +- .../Folders/Commands/DisableFolder.cs | 69 - .../Folders/Commands/EnableFolder.cs | 52 - .../Folders/Commands/RemoveFolder.cs | 38 +- .../Folders/Commands/UpdateFolder.cs | 59 +- .../Queries/GetAllFolderLogsPaginated.cs | 60 + .../Folders/Queries/GetAllFoldersPaginated.cs | 46 +- .../Folders/Queries/GetFolderById.cs | 13 + .../Commands/ApproveOrRejectDocument.cs | 137 ++ .../ImportRequests/Commands/AssignDocument.cs | 104 ++ .../Commands/CheckinDocument.cs | 104 ++ .../Commands/RequestImportDocument.cs | 101 ++ .../Queries/GetAllImportRequestsPaginated.cs | 106 ++ .../Queries/GetImportRequestById.cs | 57 + src/Application/Lockers/Commands/AddLocker.cs | 45 +- .../Lockers/Commands/DisableLocker.cs | 80 -- .../Lockers/Commands/EnableLocker.cs | 62 - .../Lockers/Commands/RemoveLocker.cs | 33 +- .../Lockers/Commands/UpdateLocker.cs | 50 +- .../Queries/GetAllLockerLogsPaginated.cs | 93 ++ .../Lockers/Queries/GetAllLockersPaginated.cs | 56 +- .../Lockers/Queries/GetLockerById.cs | 18 +- src/Application/Rooms/Commands/AddRoom.cs | 22 +- src/Application/Rooms/Commands/DisableRoom.cs | 86 -- src/Application/Rooms/Commands/EnableRoom.cs | 61 - src/Application/Rooms/Commands/RemoveRoom.cs | 39 +- src/Application/Rooms/Commands/UpdateRoom.cs | 66 +- .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 61 + .../Rooms/Queries/GetAllRoomsPaginated.cs | 41 +- .../Queries/GetEmptyContainersPaginated.cs | 2 +- .../Rooms/Queries/GetRoomByDepartmentId.cs | 43 + src/Application/Rooms/Queries/GetRoomById.cs | 18 +- .../Rooms/Queries/GetRoomByStaffId.cs | 51 + src/Application/Staffs/Commands/AddStaff.cs | 76 -- .../Staffs/Commands/AssignStaff.cs | 90 ++ .../Staffs/Commands/RemoveStaff.cs | 45 - .../Staffs/Commands/RemoveStaffFromRoom.cs | 21 +- .../EventHandlers/StaffCreatedEventHandler.cs | 28 + .../Staffs/Queries/GetAllStaffsPaginated.cs | 28 +- ...{GetStaffByRoom.cs => GetStaffByRoomId.cs} | 11 +- src/Application/Users/Commands/AddUser.cs | 34 +- src/Application/Users/Commands/DisableUser.cs | 47 - src/Application/Users/Commands/EnableUser.cs | 12 - src/Application/Users/Commands/UpdateUser.cs | 41 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 61 + .../Users/Queries/GetAllUsersPaginated.cs | 48 +- src/Application/Users/Queries/GetUserById.cs | 16 +- src/Domain/Common/BaseLoggingEntity.cs | 14 + src/Domain/Entities/Department.cs | 3 +- src/Domain/Entities/Logging/DocumentLog.cs | 9 + src/Domain/Entities/Logging/FolderLog.cs | 9 + src/Domain/Entities/Logging/LockerLog.cs | 9 + src/Domain/Entities/Logging/RequestLog.cs | 10 + src/Domain/Entities/Logging/RoomLog.cs | 8 + src/Domain/Entities/Logging/UserLog.cs | 7 + src/Domain/Entities/Physical/Borrow.cs | 5 +- src/Domain/Entities/Physical/Document.cs | 6 +- src/Domain/Entities/Physical/Folder.cs | 2 +- src/Domain/Entities/Physical/ImportRequest.cs | 16 + src/Domain/Entities/Physical/Locker.cs | 2 +- src/Domain/Entities/Physical/Permission.cs | 14 + src/Domain/Entities/Physical/Room.cs | 2 +- src/Domain/Enums/RequestType.cs | 7 + src/Domain/Events/StaffCreatedEvent.cs | 16 + src/Domain/Statuses/BorrowRequestStatus.cs | 6 +- src/Domain/Statuses/ImportRequestStatus.cs | 9 + src/Infrastructure/ConfigureServices.cs | 7 +- .../JweAuthenticationHandler.cs | 4 + .../Authorization/RequiresRoleAttribute.cs | 4 +- .../Identity/IdentityService.cs | 19 +- src/Infrastructure/Infrastructure.csproj | 1 + .../Persistence/ApplicationDbContext.cs | 11 + .../Persistence/ApplicationDbContextSeed.cs | 20 +- .../Configurations/BorrowConfiguration.cs | 2 +- .../Configurations/DocumentConfiguration.cs | 5 +- .../ImportRequestConfiguration.cs | 43 + .../Configurations/PermissionConfiguration.cs | 16 + .../Configurations/RoomConfiguration.cs | 7 +- .../Configurations/UserLogConfiguration.cs | 20 + .../20230612222216_Logging.Designer.cs | 879 +++++++++++++ .../Migrations/20230612222216_Logging.cs | 381 ++++++ .../20230612223900_Permission.Designer.cs | 917 ++++++++++++++ .../Migrations/20230612223900_Permission.cs | 52 + .../20230612230312_RequestLog.Designer.cs | 965 ++++++++++++++ .../Migrations/20230612230312_RequestLog.cs | 60 + ...30612231014_DocumentVisibility.Designer.cs | 968 ++++++++++++++ .../20230612231014_DocumentVisibility.cs | 29 + .../20230612231759_UserLog.Designer.cs | 1015 +++++++++++++++ .../Migrations/20230612231759_UserLog.cs | 60 + ...0230613140433_RequestLogReason.Designer.cs | 1019 +++++++++++++++ .../20230613140433_RequestLogReason.cs | 29 + ...0230614084428_PermissionExpiry.Designer.cs | 1022 +++++++++++++++ .../20230614084428_PermissionExpiry.cs | 30 + ...6101721_DepartmentHasManyRooms.Designer.cs | 1021 +++++++++++++++ .../20230616101721_DepartmentHasManyRooms.cs | 37 + ...0230618044742_AddImportRequest.Designer.cs | 1076 ++++++++++++++++ .../20230618044742_AddImportRequest.cs | 63 + ...8051340_AddImportRequestReason.Designer.cs | 1080 ++++++++++++++++ .../20230618051340_AddImportRequestReason.cs | 29 + ...133410_LoggingNowHasBaseObject.Designer.cs | 1110 +++++++++++++++++ .../20230618133410_LoggingNowHasBaseObject.cs | 139 +++ ...20131154_LoggingNowHasObjectId.Designer.cs | 1060 ++++++++++++++++ .../20230620131154_LoggingNowHasObjectId.cs | 158 +++ .../20230621135501_AddMoreReason.Designer.cs | 1068 ++++++++++++++++ .../20230621135501_AddMoreReason.cs | 60 + .../ApplicationDbContextModelSnapshot.cs | 441 ++++++- .../Services/DateTimeService.cs | 8 + .../Services/PermissionManager.cs | 94 ++ .../BaseClassFixture.cs | 2 +- .../Commands/ApproveBorrowRequestTests.cs | 12 +- .../Borrows/Commands/BorrowDocumentTests.cs | 20 +- .../Borrows/Commands/UpdateBorrowTests.cs | 12 +- .../Folders/Commands/DisableFolderTests.cs | 119 -- .../Folders/Commands/EnableFolderTests.cs | 90 -- .../Lockers/Commands/DisableLockerTests.cs | 88 -- .../Lockers/Commands/EnableLockerTests.cs | 86 -- .../Rooms/Commands/DisableRoomTests.cs | 123 -- .../Rooms/Commands/EnableRoomTests.cs | 94 -- .../Staffs/Commands/RemoveStaffTests.cs | 60 - .../Queries/GetAllStaffsPaginatedTests.cs | 3 +- .../Staffs/Queries/GetStaffByRoomTests.cs | 6 +- .../Common/Mappings/MappingTests.cs | 4 + 225 files changed, 19674 insertions(+), 2235 deletions(-) create mode 100644 src/Api/Controllers/ImportRequestsController.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs rename src/Api/Controllers/Payload/Requests/Borrows/{GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs => GetAllBorrowRequestsPaginatedQueryParameters.cs} (59%) create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs create mode 100644 src/Api/Services/ExpiryPermissionService.cs delete mode 100644 src/Application/Borrows/Commands/ApproveBorrowRequest.cs create mode 100644 src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs delete mode 100644 src/Application/Borrows/Commands/RejectBorrowRequest.cs create mode 100644 src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs create mode 100644 src/Application/Common/Interfaces/IDateTimeProvider.cs create mode 100644 src/Application/Common/Interfaces/IPermissionManager.cs create mode 100644 src/Application/Common/Messages/DocumentLogMessages.cs create mode 100644 src/Application/Common/Messages/FolderLogMessage.cs create mode 100644 src/Application/Common/Messages/LockerLogMessage.cs create mode 100644 src/Application/Common/Messages/RequestLogMessages.cs create mode 100644 src/Application/Common/Messages/RoomLogMessage.cs create mode 100644 src/Application/Common/Messages/UserLogMessages.cs create mode 100644 src/Application/Common/Models/Dtos/BaseDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/UserLogDto.cs create mode 100644 src/Application/Common/Models/Dtos/Physical/PermissionDto.cs create mode 100644 src/Application/Common/Models/Operations/DocumentOperation.cs delete mode 100644 src/Application/Departments/Commands/UpdateDepartment.cs create mode 100644 src/Application/Documents/Commands/ShareDocument.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs create mode 100644 src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs create mode 100644 src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs create mode 100644 src/Application/Documents/Queries/GetPermissions.cs create mode 100644 src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs delete mode 100644 src/Application/Folders/Commands/DisableFolder.cs delete mode 100644 src/Application/Folders/Commands/EnableFolder.cs create mode 100644 src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs create mode 100644 src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs create mode 100644 src/Application/ImportRequests/Commands/AssignDocument.cs create mode 100644 src/Application/ImportRequests/Commands/CheckinDocument.cs create mode 100644 src/Application/ImportRequests/Commands/RequestImportDocument.cs create mode 100644 src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs create mode 100644 src/Application/ImportRequests/Queries/GetImportRequestById.cs delete mode 100644 src/Application/Lockers/Commands/DisableLocker.cs delete mode 100644 src/Application/Lockers/Commands/EnableLocker.cs create mode 100644 src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs delete mode 100644 src/Application/Rooms/Commands/DisableRoom.cs delete mode 100644 src/Application/Rooms/Commands/EnableRoom.cs create mode 100644 src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs create mode 100644 src/Application/Rooms/Queries/GetRoomByDepartmentId.cs create mode 100644 src/Application/Rooms/Queries/GetRoomByStaffId.cs delete mode 100644 src/Application/Staffs/Commands/AddStaff.cs create mode 100644 src/Application/Staffs/Commands/AssignStaff.cs delete mode 100644 src/Application/Staffs/Commands/RemoveStaff.cs create mode 100644 src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs rename src/Application/Staffs/Queries/{GetStaffByRoom.cs => GetStaffByRoomId.cs} (77%) delete mode 100644 src/Application/Users/Commands/DisableUser.cs delete mode 100644 src/Application/Users/Commands/EnableUser.cs create mode 100644 src/Application/Users/Queries/GetAllUserLogsPaginated.cs create mode 100644 src/Domain/Common/BaseLoggingEntity.cs create mode 100644 src/Domain/Entities/Logging/DocumentLog.cs create mode 100644 src/Domain/Entities/Logging/FolderLog.cs create mode 100644 src/Domain/Entities/Logging/LockerLog.cs create mode 100644 src/Domain/Entities/Logging/RequestLog.cs create mode 100644 src/Domain/Entities/Logging/RoomLog.cs create mode 100644 src/Domain/Entities/Logging/UserLog.cs create mode 100644 src/Domain/Entities/Physical/ImportRequest.cs create mode 100644 src/Domain/Entities/Physical/Permission.cs create mode 100644 src/Domain/Enums/RequestType.cs create mode 100644 src/Domain/Events/StaffCreatedEvent.cs create mode 100644 src/Domain/Statuses/ImportRequestStatus.cs create mode 100644 src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs create mode 100644 src/Infrastructure/Services/DateTimeService.cs create mode 100644 src/Infrastructure/Services/PermissionManager.cs delete mode 100644 tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs delete mode 100644 tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs delete mode 100644 tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs delete mode 100644 tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs delete mode 100644 tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 4c4f747f..0a8f60f0 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -14,6 +14,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services { // Register services services.AddServices(); + services.AddHostedService(); services.AddControllers(opt => opt.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()))); diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 484c3949..0388c57a 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -1,8 +1,11 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Borrows; using Application.Borrows.Commands; using Application.Borrows.Queries; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Infrastructure.Identity.Authorization; @@ -10,6 +13,7 @@ namespace Api.Controllers; +[Route("api/v1/documents/[controller]")] public class BorrowsController : ApiControllerBase { private readonly ICurrentUserService _currentUserService; @@ -28,7 +32,8 @@ public BorrowsController(ICurrentUserService currentUserService) [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> BorrowDocument([FromBody] BorrowDocumentRequest request) + public async Task>> BorrowDocument( + [FromBody] BorrowDocumentRequest request) { var borrowerId = _currentUserService.GetCurrentUser().Id; var command = new BorrowDocument.Command() @@ -37,7 +42,7 @@ public async Task>> BorrowDocument([FromBody] Bor DocumentId = request.DocumentId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, - Reason = request.Reason, + BorrowReason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -53,7 +58,8 @@ public async Task>> BorrowDocument([FromBody] Bor [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> GetById([FromRoute] Guid borrowId) + public async Task>> GetById( + [FromRoute] Guid borrowId) { var user = _currentUserService.GetCurrentUser(); var command = new GetBorrowRequestById.Query() @@ -65,47 +71,25 @@ public async Task>> GetById([FromRoute] Guid borr return Ok(Result.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Staff)] - [HttpGet("staffs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsStaffPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters queryParameters) - { - var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); - var command = new GetAllBorrowRequestsPaginated.Query() - { - DepartmentId = departmentId, - EmployeeId = queryParameters.EmployeeId, - DocumentId = queryParameters.DocumentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - /// - /// Get all borrow requests as admin paginated + /// /// /// /// - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsAdminPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters queryParameters) + public async Task>>> GetAllRequestsPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new GetAllBorrowRequestsPaginated.Query() { - DepartmentId = queryParameters.DepartmentId, + CurrentUser = currentUser, + RoomId = queryParameters.RoomId, EmployeeId = queryParameters.EmployeeId, DocumentId = queryParameters.DocumentId, Page = queryParameters.Page, @@ -118,99 +102,28 @@ public async Task>>> GetAllRequests } /// - /// Get all borrow requests as employee paginated - /// - /// - /// - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("employees")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsEmployeePaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) - { - var userId = _currentUserService.GetCurrentUser().Id; - var command = new GetAllBorrowRequestsPaginated.Query() - { - EmployeeId = userId, - DocumentId = queryParameters.DocumentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get all borrow requests for a document paginated - /// - /// - /// - /// - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("documents/{documentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsForDocumentPaginated( - [FromRoute] Guid documentId, - [FromQuery] GetAllBorrowRequestsPaginatedForDocumentQueryParameters queryParameters) - { - var command = new GetAllBorrowRequestsPaginated.Query() - { - DocumentId = documentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - Status = queryParameters.Status, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - - /// - /// Approve a borrow request + /// Approve or Reject a borrow request /// /// Id of the borrow request to be approved + /// /// A BorrowDto of the approved borrow request [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("approve/{borrowId:guid}")] + [HttpPut("staffs/{borrowId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> ApproveRequest([FromRoute] Guid borrowId) - { - var command = new ApproveBorrowRequest.Command() - { - BorrowId = borrowId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Reject a borrow request - /// - /// Id of the borrow request to be rejected - /// A BorrowDto of the rejected borrow request - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("reject/{borrowId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RejectRequest([FromRoute] Guid borrowId) + public async Task>> ApproveOrRejectRequest( + [FromRoute] Guid borrowId, + [FromBody] ApproveOrRejectBorrowRequestRequest request) { - var command = new RejectBorrowRequest.Command() + var performingUserId = _currentUserService.GetId(); + var command = new ApproveOrRejectBorrowRequest.Command() { + CurrentUserId = performingUserId, BorrowId = borrowId, + StaffReason = request.StaffReason, + Decision = request.Decision }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -227,10 +140,13 @@ public async Task>> RejectRequest([FromRoute] Gui [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Checkout([FromRoute] Guid borrowId) + public async Task>> Checkout( + [FromRoute] Guid borrowId) { + var currentStaff = _currentUserService.GetCurrentUser(); var command = new CheckoutDocument.Command() { + CurrentStaff = currentStaff, BorrowId = borrowId, }; var result = await Mediator.Send(command); @@ -248,10 +164,13 @@ public async Task>> Checkout([FromRoute] Guid bor [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Return([FromRoute] Guid documentId) + public async Task>> Return( + [FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new ReturnDocument.Command() { + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(command); @@ -274,12 +193,14 @@ public async Task>> Update( [FromRoute] Guid borrowId, [FromBody] UpdateBorrowRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateBorrow.Command() { + CurrentUser = currentUser, BorrowId = borrowId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, - Reason = request.Reason, + BorrowReason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -296,13 +217,38 @@ public async Task>> Update( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Cancel([FromRoute] Guid borrowId) + public async Task>> Cancel( + [FromRoute] Guid borrowId) { + var currentUserId = _currentUserService.GetId(); var command = new CancelBorrowRequest.Command() { + CurrentUserId = currentUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Get all logs related to requests. + /// + /// Query parameters + /// A list of RequestLogsDtos. + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllRequestLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllRequestLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index bf01c28a..1292c8e1 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -1,9 +1,12 @@ using Api.Controllers.Payload.Requests.Departments; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; using Application.Departments.Commands; using Application.Departments.Queries; using Application.Identity; +using Application.Rooms.Queries; using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -12,29 +15,67 @@ namespace Api.Controllers; public class DepartmentsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public DepartmentsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// - /// Get back a department based on its id + /// Get back a room based on its department id /// /// id of the department to be retrieved /// A DepartmentDto of the retrieved department + [RequiresRole( + IdentityData.Roles.Admin, + IdentityData.Roles.Staff, + IdentityData.Roles.Employee)] [HttpGet("{departmentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid departmentId) + public async Task>> GetById( + [FromRoute] Guid departmentId) { + var role = _currentUserService.GetRole(); + var userDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetDepartmentById.Query() { - DepartmentId = departmentId + UserRole = role, + UserDepartmentId = userDepartmentId, + DepartmentId = departmentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + /// + /// Get back a department based on its id + /// + /// id of the department to be retrieved + /// A DepartmentDto of the retrieved department + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("{departmentId:guid}/rooms")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetRoomByDepartmentId( + [FromRoute] Guid departmentId) + { + var query = new GetRoomByDepartmentId.Query() + { + DepartmentId = departmentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + /// /// Get all documents /// /// A list of DocumentDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -54,7 +95,8 @@ public async Task>>> GetAll() [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Add([FromBody] AddDepartmentRequest request) + public async Task>> Add( + [FromBody] AddDepartmentRequest request) { var command = new AddDepartment.Command() { @@ -64,28 +106,6 @@ public async Task>> Add([FromBody] AddDepartm return Ok(Result.Succeed(result)); } - /// - /// Update a department - /// - /// Id of the department to be updated - /// Update department details - /// A DepartmentDto of the updated department - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("{departmentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Update([FromRoute] Guid departmentId, [FromBody] UpdateDepartmentRequest request) - { - var command = new UpdateDepartment.Command() - { - DepartmentId = departmentId, - Name = request.Name - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// /// Delete a department /// diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2a2f369f..2d500422 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,5 +1,9 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Documents; +using Application.Common.Extensions; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; @@ -11,19 +15,30 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public DocumentsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a document by id /// /// Id of the document to be retrieved /// A DocumentDto of the retrieved document + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Staff)] [HttpGet("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid documentId) + public async Task>> GetById( + [FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetDocumentById.Query() { + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(query); @@ -35,6 +50,7 @@ public async Task>> GetById([FromRoute] Guid do /// /// Get all documents query parameters /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -43,8 +59,17 @@ public async Task>> GetById([FromRoute] Guid do public async Task>>> GetAllPaginated( [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); + Guid? currentStaffRoomId = null; + if (currentUser.Role.IsStaff()) + { + currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); + } var query = new GetAllDocumentsPaginated.Query() { + CurrentUser = currentUser, + CurrentStaffRoomId = currentStaffRoomId, + UserId = queryParameters.UserId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, FolderId = queryParameters.FolderId, @@ -53,11 +78,46 @@ public async Task>>> GetAllPagina Size = queryParameters.Size, SortBy = queryParameters.SortBy, SortOrder = queryParameters.SortOrder, + IsPrivate = queryParameters.IsPrivate, + DocumentStatus = queryParameters.DocumentStatus, + Role = queryParameters.UserRole, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + + /// + /// Get all documents paginated + /// + /// Get all documents query parameters + /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllForEmployeePaginated( + [FromQuery] GetAllDocumentsForEmployeePaginatedQueryParameters queryParameters) + { + var currentUserId = _currentUserService.GetId(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var query = new GetAllDocumentsForEmployeePaginated.Query() + { + CurrentUserId = currentUserId, + CurrentUserDepartmentId = currentUserDepartmentId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + DocumentStatus = queryParameters.DocumentStatus, + IsPrivate = queryParameters.IsPrivate, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get all document types /// @@ -77,17 +137,26 @@ public async Task>>> GetAllDocumentTypes /// /// Import document details /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Import([FromBody] ImportDocumentRequest request) + public async Task>> Import( + [FromBody] ImportDocumentRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); + var currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); + if (currentUser.Department is null) + { + return Forbid(); + } var command = new ImportDocument.Command() { + CurrentUser = currentUser, + CurrentStaffRoomId = currentStaffRoomId, Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, @@ -104,20 +173,29 @@ public async Task>> Import([FromBody] ImportDoc /// Id of the document to be updated /// Update document details /// A DocumentDto of the updated document - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpPut("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) + public async Task>> Update( + [FromRoute] Guid documentId, + [FromBody] UpdateDocumentRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); + if (currentUser.Department is null) + { + return Forbid(); + } var query = new UpdateDocument.Command() { + CurrentUser = currentUser, DocumentId = documentId, Title = request.Title, Description = request.Description, - DocumentType = request.DocumentType + DocumentType = request.DocumentType, + IsPrivate = request.IsPrivate, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -127,18 +205,101 @@ public async Task>> Update([FromRoute] Guid doc /// Delete a document /// /// Id of the document to be deleted - /// A DocumentDto of the deleted document + /// A DocumentDto of the deleted document + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpDelete("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Delete([FromRoute] Guid documentId) + public async Task>> Delete( + [FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); + if (currentUser.Role.IsStaff() + && currentUser.Department is null) + { + return Forbid(); + } var query = new DeleteDocument.Command() { + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Get permissions for an employee of a specific document + /// + /// Id of the document to be getting permissions from + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{documentId:guid}/permissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetPermissions( + [FromRoute] Guid documentId) + { + var performingUser = _currentUserService.GetCurrentUser(); + var query = new GetPermissions.Query() + { + CurrentUser = performingUser, + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Share permissions for an employee of a specific document + /// + /// Id of the document + /// + /// A DocumentDto + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("{documentId:guid}/permissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> SharePermissions( + [FromRoute] Guid documentId, + [FromBody] SharePermissionsRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new ShareDocument.Command() + { + CurrentUser = currentUser, + DocumentId = documentId, + UserId = request.UserId, + CanRead = request.CanRead, + CanBorrow = request.CanBorrow, + ExpiryDate = request.ExpiryDate, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all log of document + /// + /// + /// Paginated list of DocumentLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllDocumentLogsPaginated.Query() + { + DocumentId = queryParameters.ObjectId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index a08b36cc..153367ab 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,5 +1,9 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Folders; +using Application.Common.Extensions; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands; using Application.Folders.Queries; @@ -11,6 +15,13 @@ namespace Api.Controllers; public class FoldersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public FoldersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a folder by id /// @@ -23,8 +34,12 @@ public class FoldersController : ApiControllerBase [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid folderId) { + var currentUserRole = _currentUserService.GetRole(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetFolderById.Query() { + CurrentUserRole = currentUserRole, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, }; var result = await Mediator.Send(query); @@ -43,8 +58,12 @@ public async Task>> GetById([FromRoute] Guid fold public async Task>>> GetAllPaginated( [FromQuery] GetAllFoldersPaginatedQueryParameters queryParameters) { + var currentUserRole = _currentUserService.GetRole(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetAllFoldersPaginated.Query() { + CurrentUserRole = currentUserRole, + CurrentStaffRoomId = staffRoomId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, SearchTerm = queryParameters.SearchTerm, @@ -62,7 +81,7 @@ public async Task>>> GetAllPaginate /// /// Add folder details /// A FolderDto of the added folder - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -71,8 +90,12 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> AddFolder([FromBody] AddFolderRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var command = new AddFolder.Command() { + CurrentUser = currentUser, + CurrentStaffRoomId = staffRoomId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -87,7 +110,7 @@ public async Task>> AddFolder([FromBody] AddFolde /// /// Id of the folder to be removed /// A FolderDto of the removed folder - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpDelete("{folderId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -96,58 +119,22 @@ public async Task>> AddFolder([FromBody] AddFolde [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveFolder([FromRoute] Guid folderId) { - var command = new RemoveFolder.Command() + var currentUser = _currentUserService.GetCurrentUser(); + Guid? staffRoomId = null; + if (currentUser.Role.IsStaff()) { - FolderId = folderId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Enable a folder - /// - /// Id of the folder to be enabled - /// A FolderDto of the enabled folder - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable/{folderId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableFolder([FromRoute] Guid folderId) - { - var command = new EnableFolder.Command() + staffRoomId = _currentUserService.GetCurrentRoomForStaff(); + } + var command = new RemoveFolder.Command() { + CurrentUser = currentUser, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - /// - /// Disable a folder - /// - /// Id of the disabled folder - /// A FolderDto of the disabled folder - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable/{folderId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableFolder([FromRoute] Guid folderId) - { - var command = new DisableFolder.Command() - { - FolderId = folderId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// /// Update a folder /// @@ -159,10 +146,16 @@ public async Task>> DisableFolder([FromRoute] Gui [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) + public async Task>> Update( + [FromRoute] Guid folderId, + [FromBody] UpdateFolderRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var command = new UpdateFolder.Command() { + CurrentUser = currentUser, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, Name = request.Name, Description = request.Description, @@ -171,4 +164,27 @@ public async Task>> Update([FromRoute] Guid folde var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllFolderLogsPaginated.Query() + { + FolderId = queryParameters.ObjectId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs new file mode 100644 index 00000000..dc7f0bfc --- /dev/null +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -0,0 +1,187 @@ +using Api.Controllers.Payload.Requests.Documents; +using Api.Controllers.Payload.Requests.ImportRequests; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Application.ImportRequests.Commands; +using Application.ImportRequests.Queries; +using Domain.Enums; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +[Route("api/v1/documents/[controller]")] +public class ImportRequestsController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + + public ImportRequestsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// Get an import request by id. + /// + /// Id of the request> + /// An ImportRequestDto of the request + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet("{importRequestId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetImportRequestById( + [FromRoute] Guid importRequestId) + { + var currentUserId = _currentUserService.GetId(); + var currentUserRole = _currentUserService.GetRole(); + var currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new GetImportRequestById.Query() + { + CurrentUserId = currentUserId, + CurrentUserRole = currentUserRole, + CurrentStaffRoomId = currentStaffRoomId, + RequestId = importRequestId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet] + public async Task>>> GetAllImportRequestsPaginated( + [FromQuery] GetAllImportRequestsPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllImportRequestsPaginated.Query() + { + CurrentUser = currentUser, + SearchTerm = queryParameters.SearchTerm, + RoomId = queryParameters.RoomId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Request to import a document + /// + /// Import document request details + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RequestImport( + [FromBody] RequestImportDocumentRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new RequestImportDocument.Command() + { + Title = request.Title, + Description = request.Description, + DocumentType = request.DocumentType, + ImportReason = request.ImportReason, + IsPrivate = request.IsPrivate, + Issuer = currentUser, + RoomId = request.RoomId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Approve or reject a document request + /// + /// Id of the document to be approved + /// + /// A DocumentDto of the approved document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut("{importRequestId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> ApproveOrReject( + [FromRoute] Guid importRequestId, + [FromBody] ApproveOrRejectImportRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new ApproveOrRejectDocument.Command() + { + CurrentUser = currentUser, + ImportRequestId = importRequestId, + StaffReason = request.StaffReason, + Decision = request.Decision, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Assign a document to a folder + /// + /// Id of the import request to be rejected or approved + /// + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut("assign/{importRequestId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Assign( + [FromRoute] Guid importRequestId, + [FromBody] AssignDocumentToFolderRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new AssignDocument.Command() + { + CurrentUser = currentUser, + StaffRoomId = staffRoomId, + ImportRequestId = importRequestId, + FolderId = request.FolderId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Checkin a document + /// + /// + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut("checkin/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkin( + [FromRoute] Guid documentId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new CheckinDocument.Command() + { + CurrentUser = currentUser, + DocumentId = documentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 2445df95..fb87d0ee 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,5 +1,8 @@ -using Api.Controllers.Payload.Requests.Lockers; +using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Lockers; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Lockers.Commands; @@ -11,19 +14,32 @@ namespace Api.Controllers; public class LockersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public LockersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a locker by id /// /// Id of the locker to be retrieved /// A LockerDto of the retrieved locker + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid lockerId) + public async Task>> GetById( + [FromRoute] Guid lockerId) { + var currentUserRole = _currentUserService.GetRole(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetLockerById.Query() { + CurrentUserRole = currentUserRole, + CurrentStaffRoomId = staffRoomId, LockerId = lockerId, }; var result = await Mediator.Send(query); @@ -35,14 +51,19 @@ public async Task>> GetById([FromRoute] Guid lock /// /// Get all lockers paginated query parameters /// A paginated list of LockerDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllLockersPaginated.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, RoomId = queryParameters.RoomId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, @@ -66,10 +87,13 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Add([FromBody] AddLockerRequest request) + public async Task>> Add( + [FromBody] AddLockerRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddLocker.Command() { + CurrentUser = currentUser, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -84,6 +108,7 @@ public async Task>> Add([FromBody] AddLockerReque /// /// Id of the locker to be removed /// A LockerDto of the removed locker + [RequiresRole(IdentityData.Roles.Admin)] [HttpDelete("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -91,8 +116,10 @@ public async Task>> Add([FromBody] AddLockerReque [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Remove([FromRoute] Guid lockerId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveLocker.Command() { + CurrentUser = currentUser, LockerId = lockerId, }; var result = await Mediator.Send(command); @@ -100,70 +127,58 @@ public async Task>> Remove([FromRoute] Guid locke } /// - /// Enable a locker + /// Update a locker /// - /// Id of the locker to be enabled - /// A LockerDto of the enabled locker + /// Id of the locker to be updated + /// Update locker details + /// A LockerDto of the updated locker [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable/{lockerId:guid}")] + [HttpPut("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Enable([FromRoute] Guid lockerId) + public async Task>> Update( + [FromRoute] Guid lockerId, + [FromBody] UpdateLockerRequest request) { - var command = new EnableLocker.Command() + var currentUser = _currentUserService.GetCurrentUser(); + var command = new UpdateLocker.Command() { + CurrentUser = currentUser, LockerId = lockerId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } /// - /// Disable a locker - /// - /// Id of the locker to be disabled - /// A LockerDto of the disabled locker - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable/{lockerId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Disable([FromRoute] Guid lockerId) - { - var command = new DisableLocker.Command() - { - LockerId = lockerId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Update a locker + /// Get all logs related to locker /// - /// Id of the locker to be updated - /// Update locker details - /// A LockerDto of the updated locker - [HttpPut("{lockerId:guid}")] + /// Query parameters + /// A list of LockerLogsDtos + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { - var command = new UpdateLocker.Command() + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var query = new GetAllLockerLogsPaginated.Query() { - LockerId = lockerId, - Name = request.Name, - Description = request.Description, - Capacity = request.Capacity, + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + LockerId = queryParameters.ObjectId }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); } } diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs new file mode 100644 index 00000000..7cd10e4c --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs @@ -0,0 +1,7 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class ApproveOrRejectBorrowRequestRequest +{ + public string StaffReason { get; set; } + public string Decision { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs similarity index 59% rename from src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs rename to src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs index 8ea8eca1..aa844929 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs @@ -3,12 +3,12 @@ namespace Api.Controllers.Payload.Requests.Borrows; /// /// Query parameters for getting all borrow requests with pagination as admin /// -public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQueryParameters +public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParameters { /// - /// Id of the department to get borrow requests in + /// Id of the room to get borrow requests in /// - public Guid? DepartmentId { get; set; } + public Guid? RoomId { get; set; } public Guid? DocumentId { get; set; } public Guid? EmployeeId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs new file mode 100644 index 00000000..dcbfbfce --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class RejectRequest +{ + public string Reason { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs new file mode 100644 index 00000000..5281b1e5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs @@ -0,0 +1,7 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class ApproveOrRejectImportRequest +{ + public string Decision { get; set; } = null!; + public string StaffReason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs new file mode 100644 index 00000000..bcca52f5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class AssignDocumentToFolderRequest +{ + public Guid FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs new file mode 100644 index 00000000..9b6be2b0 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllDocumentsForEmployeePaginatedQueryParameters : PaginatedQueryParameters +{ + public Guid? UserId { get; set; } + public string? SearchTerm { get; set; } + public string? DocumentStatus { get; set; } + public bool IsPrivate { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs new file mode 100644 index 00000000..375a6597 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs @@ -0,0 +1,17 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllDocumentsForStaffPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the locker to find documents in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to find documents in + /// + public Guid? FolderId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index 7b97fc8f..94cca9b8 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -5,6 +5,7 @@ namespace Api.Controllers.Payload.Requests.Documents; /// public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters { + public Guid? UserId { get; set; } /// /// Id of the room to find documents in /// @@ -21,4 +22,7 @@ public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } + public string? DocumentStatus { get; set; } + public string? UserRole { get; set; } + public bool? IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs new file mode 100644 index 00000000..ed060421 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllIssuedPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs new file mode 100644 index 00000000..a09e0eee --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetDocumentsOfUserPaginatedQueryParameters : PaginatedQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs new file mode 100644 index 00000000..3f7d6472 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Query parameters for getting all documents that belong to an employee +/// +public class GetSelfDocumentsPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs new file mode 100644 index 00000000..1e9c4b80 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class RejectImportRequest +{ + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs new file mode 100644 index 00000000..c673896d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -0,0 +1,24 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Request details to import a document +/// +public class RequestImportDocumentRequest +{ + /// + /// Title of the document to be imported + /// + public string Title { get; set; } = null!; + /// + /// Description of the document to be imported + /// + public string? Description { get; set; } + /// + /// Document type of the document to be imported + /// + public string DocumentType { get; set; } = null!; + public string ImportReason { get; set; } = null!; + + public Guid RoomId { get; set; } + public bool IsPrivate { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs new file mode 100644 index 00000000..6a26ec1b --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class SharePermissionsRequest +{ + public Guid UserId { get; set; } + public bool CanRead { get; set; } + public bool CanBorrow { get; set; } + public DateTime ExpiryDate { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs index a31cec88..97626d8a 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs @@ -17,4 +17,5 @@ public class UpdateDocumentRequest /// New document type of the document to be updated /// public string DocumentType { get; set; } = null!; + public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs new file mode 100644 index 00000000..59019d0d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -0,0 +1,25 @@ +namespace Api.Controllers.Payload.Requests; + +/// +/// Get all logs paginated +/// +public class GetAllLogsPaginatedQueryParameters +{ + /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } + + /// + /// User Id + /// + public Guid? ObjectId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs new file mode 100644 index 00000000..c66c1956 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs @@ -0,0 +1,10 @@ +namespace Api.Controllers.Payload.Requests.ImportRequests; + +/// +/// +/// +public class GetAllImportRequestsPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } + public Guid? RoomId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs index 67988e72..21374f9a 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs @@ -8,17 +8,17 @@ public class AddLockerRequest /// /// Name of the locker to be updated /// - public string Name { get; init; } = null!; + public string Name { get; set; } = null!; /// /// Description of the locker to be updated /// - public string? Description { get; init; } + public string? Description { get; set; } /// /// Id of the room that this locker will be in /// - public Guid RoomId { get; init; } + public Guid RoomId { get; set; } /// /// Number of folders this locker can hold /// - public int Capacity { get; init; } + public int Capacity { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 390ae4be..3bf5e63a 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -9,4 +9,5 @@ public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } + public Guid? DepartmentId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs index 40d4965c..d277d097 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs @@ -17,4 +17,8 @@ public class UpdateRoomRequest /// New capacity of the room to be updated /// public int Capacity { get; set; } + /// + /// Room availability + /// + public bool IsAvailable { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs index 47ebbc06..6e33caa6 100644 --- a/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs @@ -8,7 +8,7 @@ public class AddStaffRequest /// /// User id of the new staff /// - public Guid UserId { get; init; } + public Guid StaffId { get; init; } /// /// Id of the room this staff will be in /// diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs new file mode 100644 index 00000000..171bce79 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Users; + +public class GetAllEmployeesPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Search term + /// + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 47db40c8..738a1017 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -8,7 +8,8 @@ public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters /// /// Id of the department to find users in /// - public Guid? DepartmentId { get; set; } + public Guid[]? DepartmentIds { get; set; } + public string? Role { get; set; } /// /// Search term /// diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs new file mode 100644 index 00000000..7b12e5bf --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Users; + +/// +/// Request details to update that user +/// +public class UpdateSelfRequest +{ + /// + /// New first name of the user to be updated + /// + public string? FirstName { get; set; } + /// + /// New last name of the user to be updated + /// + public string? LastName { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs index c30e9871..959567d4 100644 --- a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs @@ -17,4 +17,6 @@ public class UpdateUserRequest /// New position of the user to be updated /// public string? Position { get; set; } + public string Role { get; set; } = null!; + public bool IsActive { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index fa5198ee..1f1dadca 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,10 +1,14 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Lockers; using Api.Controllers.Payload.Requests.Rooms; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Rooms.Commands; using Application.Rooms.Queries; +using Application.Staffs.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -12,19 +16,32 @@ namespace Api.Controllers; public class RoomsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public RoomsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a room by id /// /// Id of the room to be retrieved /// A RoomDto of the retrieved room + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid roomId) + public async Task>> GetById( + [FromRoute] Guid roomId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetRoomById.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, RoomId = roomId, }; var result = await Mediator.Send(query); @@ -36,15 +53,18 @@ public async Task>> GetById([FromRoute] Guid roomId /// /// Get all rooms paginated details /// A paginated list of rooms - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( [FromQuery] GetAllRoomsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetAllRoomsPaginated.Query() { + CurrentUser = currentUser, + DepartmentId = queryParameters.DepartmentId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -54,14 +74,15 @@ public async Task>>> GetAllPaginated( var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + /// /// Get empty containers in a room /// + /// /// Get empty containers paginated details /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("empty-containers/{roomId:guid}")] + [HttpPost("{roomId:guid}/empty-containers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -79,6 +100,29 @@ public async Task>> GetEmptyContainer return Ok(Result>.Succeed(result)); } + /// + /// Get a staff by room + /// + /// Id of the room to retrieve staff + /// A StaffDto of the retrieved staff + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet("{roomId:guid}/staffs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetStaffByRoom( + [FromRoute] Guid roomId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetStaffByRoomId.Query() + { + CurrentUser = currentUser, + RoomId = roomId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + /// /// Add a room /// @@ -91,10 +135,13 @@ public async Task>> GetEmptyContainer [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddRoom([FromBody] AddRoomRequest request) + public async Task>> AddRoom( + [FromBody] AddRoomRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddRoom.Command() { + CurrentUser = currentUser, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -115,51 +162,13 @@ public async Task>> AddRoom([FromBody] AddRoomReque [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RemoveRoom([FromRoute] Guid roomId) + public async Task>> RemoveRoom( + [FromRoute] Guid roomId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveRoom.Command() { - RoomId = roomId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Enable a room - /// - /// Id of the room to be enabled - /// A RoomDto of the enabled room - [HttpPut("enable/{roomId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableRoom([FromRoute] Guid roomId) - { - var command = new EnableRoom.Command() - { - RoomId = roomId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Disable a room - /// - /// Id of the room to be disabled - /// A RoomDto of the disabled room - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("disable/{roomId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableRoom([FromRoute] Guid roomId) - { - var command = new DisableRoom.Command() - { + CurrentUser = currentUser, RoomId = roomId, }; var result = await Mediator.Send(command); @@ -172,21 +181,49 @@ public async Task>> DisableRoom([FromRoute] Guid ro /// Id of the room to be updated /// Update room details /// A RoomDto of the updated room + [RequiresRole(IdentityData.Roles.Admin)] [HttpPut("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) + public async Task>> Update( + [FromRoute] Guid roomId, + [FromBody] UpdateRoomRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateRoom.Command() { + CurrentUser = currentUser, RoomId = roomId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, + IsAvailable = request.IsAvailable, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Get all room logs paginated + /// + /// + /// A paginated list of RoomLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllRoomLogsPaginated.Query() + { + RoomId = queryParameters.ObjectId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index ecd2b9c8..12452352 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -1,7 +1,9 @@ using Api.Controllers.Payload.Requests.Staffs; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; +using Application.Rooms.Queries; using Application.Staffs.Commands; using Application.Staffs.Queries; using Infrastructure.Identity.Authorization; @@ -11,42 +13,53 @@ namespace Api.Controllers; public class StaffsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public StaffsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a staff by id /// /// Id of the staff to be retrieved /// A StaffDto of the retrieved staff + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{staffId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid staffId) + public async Task>> GetById( + [FromRoute] Guid staffId) { var query = new GetStaffById.Query() { - StaffId = staffId + StaffId = staffId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } /// - /// Get a staff by room + /// Get room by staff id /// - /// Id of the room to retrieve staff - /// A StaffDto of the retrieved staff - [HttpGet("get-by-room/{roomId:guid}")] + /// Id of the staff to retrieve room + /// A RoomDto of the retrieved room + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet("{staffId:guid}/rooms")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetByRoom([FromRoute] Guid roomId) + public async Task>> GetRoomByStaffId( + [FromRoute] Guid staffId) { - var query = new GetStaffByRoom.Query() + var query = new GetRoomByStaffId.Query() { - RoomId = roomId + StaffId = staffId, }; var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); + return Ok(Result.Succeed(result)); } /// @@ -54,6 +67,7 @@ public async Task>> GetByRoom([FromRoute] Guid roo /// /// Get all staffs query parameters /// A paginated list of StaffDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -73,7 +87,7 @@ public async Task>>> GetAllPaginated } /// - /// Add a staff + /// Assign a staff /// /// Add staff details /// A StaffDto of the added staff @@ -82,12 +96,15 @@ public async Task>>> GetAllPaginated [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Add([FromBody] AddStaffRequest request) + public async Task>> Assign( + [FromBody] AddStaffRequest request) { - var command = new AddStaff.Command() + var currentUser = _currentUserService.GetCurrentUser(); + var command = new AssignStaff.Command() { + CurrentUser = currentUser, RoomId = request.RoomId, - UserId = request.UserId, + StaffId = request.StaffId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -98,40 +115,22 @@ public async Task>> Add([FromBody] AddStaffRequest /// /// Id of the staff to be removed from room /// A StaffDto of the removed staff + [RequiresRole(IdentityData.Roles.Admin)] [HttpPut("{staffId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RemoveFromRoom( + public async Task>> RemoveStaffFromRoom( [FromRoute] Guid staffId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveStaffFromRoom.Command() { + CurrentUser = currentUser, StaffId = staffId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Remove a staff - /// - /// Id of the staff to be removed - /// A StaffDto of the removed staff - [HttpDelete("{staffId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Remove( - [FromRoute] Guid staffId) - { - var command = new RemoveStaff.Command() - { - StaffId = staffId - }; - - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 5b8efe5f..12ca4695 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,28 +1,44 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Users; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Identity; using Application.Users.Commands; using Application.Users.Queries; using Infrastructure.Identity.Authorization; +using MediatR; using Microsoft.AspNetCore.Mvc; namespace Api.Controllers; public class UsersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public UsersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a user by id /// /// Id of the user to be retrieved /// A UserDto of the retrieved user + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid userId) { - var query = new GetUserById.Query + var role = _currentUserService.GetRole(); + var userDepartmentId = _currentUserService.GetDepartmentId(); + var query = new GetUserById.Query() { + UserRole = role, + UserDepartmentId = userDepartmentId, UserId = userId, }; var result = await Mediator.Send(query); @@ -43,7 +59,36 @@ public async Task>>> GetAllPaginated( { var query = new GetAllUsersPaginated.Query() { - DepartmentId = queryParameters.DepartmentId, + DepartmentIds = queryParameters.DepartmentIds, + Role = queryParameters.Role, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all employees in the same department + /// + /// Query parameters + /// A list of UserDtos + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>>> GetAllEmployeesPaginated( + [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetDepartmentId(); + var query = new GetAllUsersPaginated.Query() + { + DepartmentIds = new []{ departmentId }, + Role = IdentityData.Roles.Employee, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -68,8 +113,10 @@ public async Task>>> GetAllPaginated( [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Add([FromBody] AddUserRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddUser.Command() { + CurrentUser = currentUser, Username = request.Username, Email = request.Email, FirstName = request.FirstName, @@ -81,43 +128,33 @@ public async Task>> Add([FromBody] AddUserRequest r var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Enable a user - /// - /// Id of the user to be enabled - /// A UserDto of the enabled user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost("enable/{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Enable([FromRoute] Guid userId) - { - var command = new EnableUser.Command() - { - UserId = userId - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - + /// - /// Disable a user + /// Update a user /// - /// Id of the user to be disabled - /// A UserDto of the disabled user + /// Id of the user to be updated + /// Update user details + /// A UserDto of the updated user [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("disable/{userId:guid}")] + [HttpPut("{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Disable([FromRoute] Guid userId) + public async Task>> Update( + [FromRoute] Guid userId, + [FromBody] UpdateUserRequest request) { - var command = new DisableUser.Command() + var currentUser = _currentUserService.GetCurrentUser(); + var command = new UpdateUser.Command() { + CurrentUser = currentUser, UserId = userId, + FirstName = request.FirstName, + LastName = request.LastName, + Position = request.Position, + Role = request.Role, + IsActive = request.IsActive, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -126,24 +163,52 @@ public async Task>> Disable([FromRoute] Guid userId /// /// Update a user /// - /// Id of the user to be updated /// Update user details /// A UserDto of the updated user - [HttpPut("{userId:guid}")] + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpPut("self")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) + public async Task>> UpdateSelf( + [FromBody] UpdateSelfRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateUser.Command() { - UserId = userId, + CurrentUser = currentUser, + UserId = currentUser.Id, FirstName = request.FirstName, LastName = request.LastName, - Position = request.Position, + Position = currentUser.Position, + Role = currentUser.Role, + IsActive = currentUser.IsActive, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } -} + + /// + /// Get all user related logs paginated + /// + /// Get all users related logs query parameters + /// A paginated list of UserLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllUserLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + UserId = queryParameters.ObjectId, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index c08e16ed..3a52aae5 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -100,7 +100,7 @@ private static async void HandleAuthenticationException(HttpContext context, Exc private static async void HandleUnauthorizedAccessException(HttpContext context, Exception ex) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.StatusCode = StatusCodes.Status403Forbidden; await WriteExceptionMessageAsync(context, ex); } diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 920c6361..143498a9 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -7,25 +7,32 @@ namespace Api.Services; public class CurrentUserService : ICurrentUserService { - private readonly IApplicationDbContext _context; + private readonly IApplicationDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; public CurrentUserService(IHttpContextAccessor httpContextAccessor, IApplicationDbContext context) { _httpContextAccessor = httpContextAccessor; - _context = context; + _dbContext = context; + } + + public Guid GetId() + { + var id = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId))!.Value; + return Guid.Parse(id); } public string GetRole() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); } - var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + var user = _dbContext.Users.FirstOrDefault(x => x.Username.Equals(userName)); if (user is null) { @@ -35,37 +42,24 @@ public string GetRole() return user.Role; } - public string? GetDepartment() + public Guid GetDepartmentId() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; - if (userName is null) - { - throw new UnauthorizedAccessException(); - } - - var user = _context.Users - .Include(x => x.Department) - .FirstOrDefault(x => x.Username.Equals(userName)); - - if (user is null) - { - throw new UnauthorizedAccessException(); - } - - return user.Department?.Name; + var claim = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("departmentId")); + var id = claim?.Value; + return Guid.Parse(id!); } public User GetCurrentUser() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); } - var user = _context.Users + var user = _dbContext.Users .Include(x => x.Department) .FirstOrDefault(x => x.Username.Equals(userName)); @@ -79,50 +73,35 @@ public User GetCurrentUser() public Guid? GetCurrentRoomForStaff() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; - if (userName is null) + var userIdString = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId)); + if (userIdString is null || !Guid.TryParse(userIdString.Value, out var userId)) { throw new UnauthorizedAccessException(); } - var staff = _context.Staffs + var staff = _dbContext.Staffs .Include(x => x.User) .Include(x => x.Room) - .FirstOrDefault(x => x.User.Username.Equals(userName)); - - if (staff is null) - { - throw new UnauthorizedAccessException(); - } + .FirstOrDefault(x => x.Id == userId); - return staff.Room!.Id; + return staff?.Room?.Id; } public Guid? GetCurrentDepartmentForStaff() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; - if (userName is null) + var userIdString = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId)); + if (userIdString is null || !Guid.TryParse(userIdString.Value, out var userId)) { throw new UnauthorizedAccessException(); } - var staff = _context.Staffs + var staff = _dbContext.Staffs .Include(x => x.User) .Include(x => x.Room) - .FirstOrDefault(x => x.User.Username.Equals(userName)); - - if (staff is null) - { - throw new UnauthorizedAccessException(); - } - - if (staff.Room is null) - { - throw new UnauthorizedAccessException(); - } + .FirstOrDefault(x => x.Id == userId); - return staff.Room!.DepartmentId; + return staff?.Room?.DepartmentId; } } \ No newline at end of file diff --git a/src/Api/Services/ExpiryPermissionService.cs b/src/Api/Services/ExpiryPermissionService.cs new file mode 100644 index 00000000..360c108e --- /dev/null +++ b/src/Api/Services/ExpiryPermissionService.cs @@ -0,0 +1,30 @@ +using Application.Common.Interfaces; +using Domain.Entities.Physical; +using NodaTime; + +namespace Api.Services; + +public class ExpiryPermissionService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public ExpiryPermissionService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var expiredPermissions = context.Permissions.Where(x => x.ExpiryDateTime < localDateTimeNow); + context.Permissions.RemoveRange(expiredPermissions); + await context.SaveChangesAsync(stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index f1789974..2f6bfb3b 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs deleted file mode 100644 index 9d7b345d..00000000 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Borrows.Commands; - -public class ApproveBorrowRequest -{ - public record Command : IRequest - { - public Guid BorrowId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var borrowRequest = await _context.Borrows - .Include(x => x.Borrower) - .Include(x => x.Document) - .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); - if (borrowRequest is null) - { - throw new KeyNotFoundException("Borrow request does not exist."); - } - - if (borrowRequest.Document.Status is DocumentStatus.Lost) - { - borrowRequest.Status = BorrowRequestStatus.NotProcessable; - _context.Borrows.Update(borrowRequest); - await _context.SaveChangesAsync(cancellationToken); - throw new ConflictException("Document is lost. Request is unprocessable."); - } - - if (borrowRequest.Status is not BorrowRequestStatus.Pending - && borrowRequest.Status is not BorrowRequestStatus.Rejected) - { - throw new ConflictException("Request cannot be approved."); - } - - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows - .FirstOrDefaultAsync(x => - x.Document.Id == borrowRequest.Document.Id - && x.Id != borrowRequest.Id - && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - - if (existedBorrow is not null) - { - if (existedBorrow?.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && borrowRequest.BorrowTime < existedBorrow.DueTime) - { - throw new ConflictException("This document cannot be borrowed."); - } - } - - borrowRequest.Status = BorrowRequestStatus.Approved; - var result = _context.Borrows.Update(borrowRequest); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs new file mode 100644 index 00000000..45120059 --- /dev/null +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -0,0 +1,156 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Enums; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ApproveOrRejectBorrowRequest +{ + public record Command : IRequest + { + public Guid CurrentUserId { get; init; } + public Guid BorrowId { get; init; } + public string Decision { get; init; } = null!; + public string StaffReason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + if (borrowRequest.Document.Status is DocumentStatus.Lost) + { + borrowRequest.Status = BorrowRequestStatus.NotProcessable; + _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + throw new ConflictException("Document is lost. Request is unprocessable."); + } + + if (borrowRequest.Status is not (BorrowRequestStatus.Pending or BorrowRequestStatus.Rejected) + && request.Decision.IsApproval()) + { + throw new ConflictException("Request cannot be approved."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Pending + && request.Decision.IsRejection()) + { + throw new ConflictException("Request cannot be rejected."); + } + + var currentUser = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not manage a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var existedBorrows = _context.Borrows + .Where(x => + x.Document.Id == borrowRequest.Document.Id + && x.Id != borrowRequest.Id + && (x.DueTime > localDateTimeNow + || x.Status == BorrowRequestStatus.Overdue)); + + var log = new DocumentLog() + { + ObjectId = borrowRequest.Document.Id, + UserId = currentUser!.Id, + User = currentUser, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Approve, + }; + var requestLog = new RequestLog() + { + ObjectId = borrowRequest.Document.Id, + Type = RequestType.Borrow, + UserId = currentUser.Id, + User = currentUser, + Time = localDateTimeNow, + Action = RequestLogMessages.ApproveBorrow, + }; + + if (request.Decision.IsApproval()) + { + foreach (var existedBorrow in existedBorrows) + { + if ((existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut) + && (borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime)) + { + throw new ConflictException("Request cannot be approved."); + } + } + + borrowRequest.Status = BorrowRequestStatus.Approved; + } + + if (request.Decision.IsRejection()) + { + borrowRequest.Status = BorrowRequestStatus.Rejected; + log.Action = DocumentLogMessages.Borrow.Reject; + requestLog.Action = RequestLogMessages.RejectBorrow; + } + + borrowRequest.StaffReason = request.StaffReason; + borrowRequest.LastModified = localDateTimeNow; + borrowRequest.LastModifiedBy = currentUser.Id; + + var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index ad1df6fd..7fbf04d7 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,8 +1,13 @@ +using System.Runtime.InteropServices; using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; +using Domain.Events; using Domain.Statuses; using FluentValidation; using MediatR; @@ -19,7 +24,7 @@ public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.Reason) + RuleFor(x => x.BorrowReason) .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); RuleFor(x => x.BorrowFrom) @@ -37,18 +42,22 @@ public record Command : IRequest public Guid BorrowerId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } - public string Reason { get; init; } = null!; + public string BorrowReason { get; init; } = null!; } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IPermissionManager _permissionManager; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _permissionManager = permissionManager; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -73,6 +82,7 @@ public async Task Handle(Command request, CancellationToken cancellat var document = await _context.Documents .Include(x => x.Department) + .Include(x => x.Importer) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { @@ -93,29 +103,31 @@ public async Task Handle(Command request, CancellationToken cancellat // if the request is in time, meaning not overdue, // then check if its due date is less than the borrow request date, if not then check // if it's already been approved, checked out or lost, meaning - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var borrowFromTime = LocalDateTime.FromDateTime(request.BorrowFrom); + var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo); + var existedBorrows = _context.Borrows .Include(x => x.Borrower) - .FirstOrDefaultAsync(x => + .Where(x => x.Document.Id == request.DocumentId - && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - - if (existedBorrow is not null) + && (x.DueTime > localDateTimeNow + || x.Status == BorrowRequestStatus.Overdue)); + + foreach (var borrow in existedBorrows) { // Does not make sense if the same person go up and want to borrow the same document again // even if the borrow day will be after the due day - if (existedBorrow.Borrower.Id == request.BorrowerId - && existedBorrow.Status is BorrowRequestStatus.Pending - or BorrowRequestStatus.Approved) + if (borrow.Borrower.Id == request.BorrowerId + && borrow.Status is BorrowRequestStatus.Pending + or BorrowRequestStatus.Approved) { throw new ConflictException("This document is already requested borrow from the same user."); } - if (existedBorrow.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + if ((borrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut) + && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { throw new ConflictException("This document cannot be borrowed."); } @@ -125,13 +137,36 @@ or BorrowRequestStatus.CheckedOut { Borrower = user, Document = document, - BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom), - DueTime = LocalDateTime.FromDateTime(request.BorrowTo), - Reason = request.Reason, + BorrowTime = borrowFromTime, + DueTime = borrowToTime, + BorrowReason = request.BorrowReason, + StaffReason = string.Empty, Status = BorrowRequestStatus.Pending, + Created = localDateTimeNow, + CreatedBy = user.Id, + }; + + if (document.IsPrivate) + { + var isGranted = _permissionManager.IsGranted(request.DocumentId, DocumentOperation.Borrow, request.BorrowerId); + if (!isGranted) + { + throw new UnauthorizedAccessException("You don't have permission to borrow this document."); + } + entity.Status = BorrowRequestStatus.Approved; + } + + var log = new DocumentLog() + { + UserId = user.Id, + User = user, + ObjectId = document.Id, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.NewBorrowRequest, }; var result = await _context.Borrows.AddAsync(entity, cancellationToken); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index 97f111d4..b4015ffd 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Borrows.Commands; @@ -12,6 +15,7 @@ public class CancelBorrowRequest { public record Command : IRequest { + public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } } @@ -19,11 +23,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -37,13 +43,32 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Borrow request does not exist."); } - if (borrowRequest.Status is not (BorrowRequestStatus.Approved or BorrowRequestStatus.Pending)) + if (borrowRequest.Status is not BorrowRequestStatus.Pending) { throw new ConflictException("Request cannot be cancelled."); } + if (borrowRequest.Borrower.Id != request.CurrentUserId) + { + throw new ConflictException("Can not cancel other borrow request"); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var currentUser = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + var log = new DocumentLog() + { + ObjectId = borrowRequest.Document.Id, + UserId = currentUser!.Id, + User = currentUser, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.CanCel, + }; + borrowRequest.Status = BorrowRequestStatus.Cancelled; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 6578225f..9ac4d77b 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -1,10 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Borrows.Commands; @@ -12,6 +16,7 @@ public class CheckoutDocument { public record Command : IRequest { + public User CurrentStaff { get; init; } = null!; public Guid BorrowId { get; init; } } @@ -19,11 +24,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -31,7 +38,11 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowRequest = await _context.Borrows .Include(x => x.Borrower) .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) { throw new KeyNotFoundException("Borrow request does not exist."); @@ -47,10 +58,41 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be checked out."); } + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentStaff.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); borrowRequest.Status = BorrowRequestStatus.CheckedOut; borrowRequest.Document.Status = DocumentStatus.Borrowed; + borrowRequest.Document.LastModified = localDateTimeNow; + borrowRequest.Document.LastModifiedBy = request.CurrentStaff.Id; + var log = new DocumentLog() + { + ObjectId = borrowRequest.Document.Id, + UserId = request.CurrentStaff.Id, + User = request.CurrentStaff, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Checkout, + }; var result = _context.Borrows.Update(borrowRequest); _context.Documents.Update(borrowRequest.Document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs deleted file mode 100644 index e1290e83..00000000 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Borrows.Commands; - -public class RejectBorrowRequest -{ - public record Command : IRequest - { - public Guid BorrowId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var borrowRequest = await _context.Borrows - .Include(x => x.Borrower) - .Include(x => x.Document) - .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); - if (borrowRequest is null) - { - throw new KeyNotFoundException("Borrow request does not exist."); - } - - if (borrowRequest.Status is not BorrowRequestStatus.Pending) - { - throw new ConflictException("Request cannot be rejected."); - } - - borrowRequest.Status = BorrowRequestStatus.Rejected; - var result = _context.Borrows.Update(borrowRequest); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index 918a979d..e18b5c89 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -1,7 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,6 +16,7 @@ public class ReturnDocument { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } } @@ -20,11 +24,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -32,8 +38,11 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowRequest = await _context.Borrows .Include(x => x.Borrower) .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId - && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); + && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); if (borrowRequest is null) { throw new KeyNotFoundException("Borrow request does not exist."); @@ -48,11 +57,42 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("Request cannot be made."); } + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + borrowRequest.Status = BorrowRequestStatus.Returned; borrowRequest.Document.Status = DocumentStatus.Available; - borrowRequest.ActualReturnTime = LocalDateTime.FromDateTime(DateTime.Now); + borrowRequest.ActualReturnTime = localDateTimeNow; + + var log = new DocumentLog() + { + ObjectId = borrowRequest.Document.Id, + UserId = request.CurrentUser.Id, + User = request.CurrentUser, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Checkout, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); _context.Documents.Update(borrowRequest.Document); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index fe39c471..c8cd900f 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -1,7 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -19,7 +22,7 @@ public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.Reason) + RuleFor(x => x.BorrowReason) .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); RuleFor(x => x.BorrowFrom) @@ -33,21 +36,24 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid BorrowId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } - public string Reason { get; init; } = null!; + public string BorrowReason { get; init; } = null!; } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -71,31 +77,48 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Document is lost."); } - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows + if (borrowRequest.Borrower.Id != request.CurrentUser.Id) + { + throw new ConflictException("Can not update other borrow request."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var existedBorrows = _context.Borrows .Include(x => x.Borrower) - .FirstOrDefaultAsync(x => + .Where(x => x.Document.Id == borrowRequest.Document.Id && x.Id != borrowRequest.Id && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - - if (existedBorrow is not null) + || x.Status == BorrowRequestStatus.Overdue)); + + var borrowFromTime = LocalDateTime.FromDateTime(request.BorrowFrom); + var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo); + foreach (var borrow in existedBorrows) { - if (existedBorrow.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + if ((borrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut) + && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { - throw new ConflictException("This document cannot be borrowed."); + throw new ConflictException("This document cannot be updated."); } } - - borrowRequest.BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom); - borrowRequest.DueTime = LocalDateTime.FromDateTime(request.BorrowTo); - borrowRequest.Reason = request.Reason; - + borrowRequest.BorrowTime = borrowFromTime; + borrowRequest.DueTime = borrowToTime; + borrowRequest.BorrowReason = request.BorrowReason; + borrowRequest.LastModified = localDateTimeNow; + borrowRequest.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + UserId = request.CurrentUser.Id, + User = request.CurrentUser, + ObjectId = borrowRequest.Document.Id, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Update, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index c78f674a..ee822637 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -1,11 +1,11 @@ -using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; using Domain.Statuses; -using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,17 +13,10 @@ namespace Application.Borrows.Queries; public class GetAllBorrowRequestsPaginated { - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - } - } - public record Query : IRequest> { - public Guid? DepartmentId { get; init; } + public User CurrentUser { get; init; } = null!; + public Guid? RoomId { get; init; } public Guid? DocumentId { get; init; } public Guid? EmployeeId { get; init; } public int? Page { get; init; } @@ -47,6 +40,47 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUser.Role.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + + if (request.CurrentUser.Role.IsEmployee()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + if (request.EmployeeId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("User can not access this resource"); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + var borrows = _context.Borrows.AsQueryable(); borrows = borrows @@ -59,9 +93,9 @@ public async Task> Handle(Query request, .ThenInclude(t => t.Room) .ThenInclude(s => s.Department); - if (request.DepartmentId is not null) + if (request.RoomId is not null) { - borrows = borrows.Where(x => x.Document.Department!.Id == request.DepartmentId); + borrows = borrows.Where(x => x.Document.Folder!.Locker.Room.Id == request.RoomId); } if (request.EmployeeId is not null) @@ -108,5 +142,8 @@ public async Task> Handle(Query request, return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } + + private static bool RoomIsNotInSameDepartment(User user, Room room) + => user.Department?.Id != room.DepartmentId; } } \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs new file mode 100644 index 00000000..dc3176e8 --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -0,0 +1,58 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetAllRequestLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.RequestLogs + .Include(x => x.ObjectId) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .OrderByDescending(x => x.Time) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index a670cca7..71587465 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -1,4 +1,10 @@ using System.Linq.Expressions; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using AutoMapper; +using Domain.Common; +using Microsoft.EntityFrameworkCore; namespace Application.Common.Extensions; @@ -9,12 +15,12 @@ public static IQueryable OrderByCustom(this IQueryable Paginate(this IQueryable ite { return items.Skip((page - 1) * size).Take(size); } + + public static async Task> LoggingListPaginateAsync( + this IQueryable items, + int? page, + int? size, + IConfigurationProvider mapperConfiguration, + CancellationToken cancellationToken) + where TEntityDto : BaseDto, IMapFrom + where TLoggingEntity : BaseLoggingEntity + where TEntity : BaseEntity + { + var pageNumber = page is null or <= 0 ? 1 : page; + var sizeNumber = size is null or <= 0 ? 10 : size; + + var count = await items.CountAsync(cancellationToken); + var list = await items + .OrderByDescending(x => x.Time) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var mapper = mapperConfiguration.CreateMapper(); + var result = mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + + public static async Task> ListPaginateWithSortAsync( + this IQueryable items, + int? page, + int? size, + string? sortBy, + string? sortOrder, + IConfigurationProvider mapperConfiguration, + CancellationToken cancellationToken) + where TEntityDto : BaseDto, IMapFrom + { + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BaseDto.Id); + } + + sortOrder ??= "asc"; + var pageNumber = page is null or <= 0 ? 1 : page; + var sizeNumber = size is null or <= 0 ? 10 : size; + + var count = await items.CountAsync(cancellationToken); + var list = await items + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var mapper = mapperConfiguration.CreateMapper(); + var result = mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } } \ No newline at end of file diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs index b2a723de..07f9a6e5 100644 --- a/src/Application/Common/Extensions/StringExtensions.cs +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -1,3 +1,5 @@ +using Application.Identity; + namespace Application.Common.Extensions; public static class StringExtensions @@ -10,4 +12,20 @@ public static bool MatchesPropertyName(this string input) return properties.Any(property => string.Equals(property.Name, input)); } + + public static bool IsAdmin(this string role) + => role.Equals(IdentityData.Roles.Admin); + + public static bool IsStaff(this string role) + => role.Equals(IdentityData.Roles.Staff); + + public static bool IsEmployee(this string role) + => role.Equals(IdentityData.Roles.Employee); + + public static bool IsApproval(this string decision) + => decision.ToLower().Trim().Equals("approve"); + + public static bool IsRejection(this string decision) + => decision.ToLower().Trim().Equals("reject"); + } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 8f12f9bd..9176b655 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,5 +1,6 @@ using Domain.Entities; using Domain.Entities.Digital; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; @@ -15,11 +16,20 @@ public interface IApplicationDbContext public DbSet Lockers { get; } public DbSet Folders { get; } public DbSet Documents { get; } + public DbSet ImportRequests { get; } public DbSet Borrows { get; } + public DbSet Permissions { get; } public DbSet UserGroups { get; } public DbSet Files { get; } public DbSet Entries { get; } + + public DbSet RoomLogs { get; } + public DbSet LockerLogs { get; } + public DbSet FolderLogs { get; } + public DbSet DocumentLogs { get; } + public DbSet RequestLogs { get; } + public DbSet UserLogs { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index 0c4aed4b..a5528245 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -4,8 +4,9 @@ namespace Application.Common.Interfaces; public interface ICurrentUserService { + Guid GetId(); string GetRole(); - string? GetDepartment(); + Guid GetDepartmentId(); User GetCurrentUser(); Guid? GetCurrentRoomForStaff(); Guid? GetCurrentDepartmentForStaff(); diff --git a/src/Application/Common/Interfaces/IDateTimeProvider.cs b/src/Application/Common/Interfaces/IDateTimeProvider.cs new file mode 100644 index 00000000..379ff470 --- /dev/null +++ b/src/Application/Common/Interfaces/IDateTimeProvider.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Interfaces; + +public interface IDateTimeProvider +{ + public DateTime DateTimeNow { get; } +} \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IPermissionManager.cs b/src/Application/Common/Interfaces/IPermissionManager.cs new file mode 100644 index 00000000..4e7c32fc --- /dev/null +++ b/src/Application/Common/Interfaces/IPermissionManager.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Operations; +using Domain.Entities; +using Domain.Entities.Physical; + +namespace Application.Common.Interfaces; + +public interface IPermissionManager +{ + bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds); + Task GrantAsync(Document document, DocumentOperation operation, User[] users, DateTime expiryDate, CancellationToken cancellationToken); + Task RevokeAsync(Guid documentId, DocumentOperation operation, Guid[] userIds, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs new file mode 100644 index 00000000..e92040f2 --- /dev/null +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -0,0 +1,30 @@ +namespace Application.Common.Messages; + +public static class DocumentLogMessages +{ + public static class Import + { + public const string NewImport = "Imported new document"; + public const string NewImportRequest = "Created new import request"; + public const string Checkin = "Checked in document"; + public const string Approve = "Document is approved to be imported"; + public const string Reject = "Rejected import request"; + public const string Assign = "Assigned to a folder"; + } + public static class Borrow + { + public const string NewBorrowRequest = "Created new borrow request"; + public const string CanCel = "Cancelled borrow request"; + public const string Approve = "Approved borrow request"; + public const string Reject = "Rejected borrow request"; + public const string Checkout = "Checked out borrow request"; + public const string Return = "Returned borrow request"; + public const string Update = "Updated borrow request"; + } + public const string Delete = "Delete document"; + public const string Update = "Updated document information"; + public static string GrantRead(string userName) => $"Share Read Permission to user {userName}"; + public static string GrantBorrow(string userName) => $"Share Borrow Permission to user {userName}"; + public static string RevokeRead(string userName) => $"Remove Read Permission to user {userName}"; + public static string RevokeBorrow(string userName) => $"Remove Borrow Permission to user {userName}"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs new file mode 100644 index 00000000..c2bffb4d --- /dev/null +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -0,0 +1,9 @@ +namespace Application.Common.Messages; + +public static class FolderLogMessage +{ + public const string Add = "Added folder"; + public const string Update = "Updated folder"; + public const string Remove = "Removed folder"; + public const string AssignDocument = "Assigned document to folder"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/LockerLogMessage.cs b/src/Application/Common/Messages/LockerLogMessage.cs new file mode 100644 index 00000000..291e4510 --- /dev/null +++ b/src/Application/Common/Messages/LockerLogMessage.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Messages; + +public static class LockerLogMessage +{ + public const string Add = "Added locker"; + public const string Update = "Updated locker"; + public const string Remove = "Removed locker"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs new file mode 100644 index 00000000..a84fbc04 --- /dev/null +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -0,0 +1,10 @@ +namespace Application.Common.Messages; + +public static class RequestLogMessages +{ + public const string ApproveImport = "Approved import request"; + public const string RejectImport = "Rejected import request"; + public const string ApproveBorrow = "Rejected borrow request"; + public const string RejectBorrow = "Rejected borrow request"; + public const string CheckInImport = "Checkin import request"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/RoomLogMessage.cs b/src/Application/Common/Messages/RoomLogMessage.cs new file mode 100644 index 00000000..90125821 --- /dev/null +++ b/src/Application/Common/Messages/RoomLogMessage.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Messages; + +public static class RoomLogMessage +{ + public const string Add = "Added room"; + public const string Update = "Updated room"; + public const string Remove = "Removed room"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/UserLogMessages.cs b/src/Application/Common/Messages/UserLogMessages.cs new file mode 100644 index 00000000..5599cb71 --- /dev/null +++ b/src/Application/Common/Messages/UserLogMessages.cs @@ -0,0 +1,16 @@ +namespace Application.Common.Messages; + +public static class UserLogMessages +{ + public static string Add(string role) => $"Added user with role {role}"; + public const string Update = "Updated user"; + public const string Disable = "Disabled user"; + + public static class Staff + { + public const string AddStaff = "Added a new staff"; + public static string AssignStaff(string roomId) => $"Assigned user to be staff of room {roomId}"; + public const string RemoveFromRoom = "Removed staff from room"; + public const string Remove = "Removed staff"; + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/BaseDto.cs b/src/Application/Common/Models/Dtos/BaseDto.cs new file mode 100644 index 00000000..28da7387 --- /dev/null +++ b/src/Application/Common/Models/Dtos/BaseDto.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Models.Dtos; + +public class BaseDto +{ + public Guid Id { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/DepartmentDto.cs b/src/Application/Common/Models/Dtos/DepartmentDto.cs index 9ae2e4e6..2a22ab44 100644 --- a/src/Application/Common/Models/Dtos/DepartmentDto.cs +++ b/src/Application/Common/Models/Dtos/DepartmentDto.cs @@ -1,19 +1,11 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; namespace Application.Common.Models.Dtos; -public class DepartmentDto : IMapFrom +public class DepartmentDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; - public Guid? RoomId { get; set; } - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember(dest => dest.RoomId, - opt => opt.MapFrom(src => src.Room!.Id)); - } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs index fe5445ab..b3882a91 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Digital; -public class EntryDto : IMapFrom +public class EntryDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; public string Path { get; set; } = null!; public FileDto? File { get; set; } diff --git a/src/Application/Common/Models/Dtos/Digital/FileDto.cs b/src/Application/Common/Models/Dtos/Digital/FileDto.cs index cf2f7531..c23b2383 100644 --- a/src/Application/Common/Models/Dtos/Digital/FileDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/FileDto.cs @@ -3,8 +3,7 @@ namespace Application.Common.Models.Dtos.Digital; -public class FileDto : IMapFrom +public class FileDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string FileType { get; set; } = null!; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs index 88fb1d13..3707c15c 100644 --- a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs @@ -3,8 +3,7 @@ namespace Application.Common.Models.Dtos.Digital; -public class UserGroupDto : IMapFrom +public class UserGroupDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs new file mode 100644 index 00000000..d05b3774 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs @@ -0,0 +1,22 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class ImportRequestDto : BaseDto, IMapFrom +{ + public IssuedRequestRoomDto Room { get; set; } = null!; + public IssuedDocumentDto Document { get; set; } = null!; + public string ImportReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; + public string Status { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs new file mode 100644 index 00000000..a16aa23f --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs @@ -0,0 +1,24 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuedDocumentDto : BaseDto, IMapFrom +{ + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string DocumentType { get; set; } = null!; + public IssuerDto? Issuer { get; set; } + public string Status { get; set; } = null!; + public bool IsPrivate { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())) + .ForMember(dest => dest.Issuer, + opt => opt.MapFrom(x => x.Importer)); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs new file mode 100644 index 00000000..6800b193 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs @@ -0,0 +1,21 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuedRequestRoomDto : BaseDto, IMapFrom +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public Guid? StaffId { get; set; } + public DepartmentDto? Department { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.StaffId, + opt => opt.MapFrom(src => src.Staff!.Id)); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs new file mode 100644 index 00000000..bb812d7e --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs @@ -0,0 +1,16 @@ +using Application.Common.Mappings; +using Domain.Entities; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuerDto : BaseDto, IMapFrom +{ + public string Username { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Role { get; set; } + public string Position { get; set; } + public bool IsActive { get; set; } + public bool IsActivated { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs new file mode 100644 index 00000000..c7098277 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class DocumentLogDto : BaseDto, IMapFrom +{ + public Guid UserId { get; set; } + public string Action { get; set; } + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } + + public void Mapping(Profile profile) + { + + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs new file mode 100644 index 00000000..f5287aea --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs @@ -0,0 +1,22 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class FolderLogDto : BaseDto, IMapFrom +{ + public string Action { get; set; } = null!; + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs new file mode 100644 index 00000000..dc532aa7 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs @@ -0,0 +1,22 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class LockerLogDto : BaseDto, IMapFrom +{ + public string Action { get; set; } = null!; + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember( dest => dest.Time, + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs new file mode 100644 index 00000000..a874bd7a --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class RequestLogDto : BaseDto, IMapFrom +{ + public string Action { get; set; } = null!; + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + public string Type { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Type, + opt => opt.MapFrom(src => src.Type.ToString())); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs new file mode 100644 index 00000000..8d05216a --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class RoomLogDto : BaseDto, IMapFrom +{ + public Guid UserId { get; set; } + public string Action { get; set; } + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } + + public void Mapping(Profile profile) + { + + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs new file mode 100644 index 00000000..83a1d38a --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs @@ -0,0 +1,22 @@ +using Application.Common.Mappings; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class UserLogDto : BaseDto, IMapFrom +{ + public string Action { get; set; } = null!; + public Guid? ObjectId { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember( dest => dest.Time, + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs index 49afc176..8199fa36 100644 --- a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs @@ -5,15 +5,15 @@ namespace Application.Common.Models.Dtos.Physical; -public class BorrowDto : IMapFrom +public class BorrowDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public Guid BorrowerId { get; set; } public Guid DocumentId { get; set; } public DateTime BorrowTime { get; set; } public DateTime DueTime { get; set; } public DateTime ActualReturnTime { get; set; } - public string Reason { get; set; } = null!; + public string BorrowReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public string Status { get; set; } = null!; public void Mapping(Profile profile) diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 6c517ce1..6d383334 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class DocumentDto : IMapFrom +public class DocumentDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; @@ -16,6 +15,7 @@ public class DocumentDto : IMapFrom public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } public string Status { get; set; } = null!; + public bool IsPrivate { get; set; } public EntryDto? Entry { get; set; } public void Mapping(Profile profile) diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs index 7420ad90..1b1e6fb9 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class DocumentItemDto : IMapFrom +public class DocumentItemDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; diff --git a/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs index 10e77646..b05091d7 100644 --- a/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs @@ -4,7 +4,7 @@ namespace Application.Common.Models.Dtos.Physical; -public class EmptyFolderDto : IMapFrom +public class EmptyFolderDto : BaseDto, IMapFrom { public Guid Id { get; set; } public string Name { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs index 6d7537c7..13135db4 100644 --- a/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class EmptyLockerDto : IMapFrom +public class EmptyLockerDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int Capacity { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/FolderDto.cs b/src/Application/Common/Models/Dtos/Physical/FolderDto.cs index 8460746b..87acaa71 100644 --- a/src/Application/Common/Models/Dtos/Physical/FolderDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/FolderDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class FolderDto : IMapFrom +public class FolderDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public LockerDto Locker { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/LockerDto.cs b/src/Application/Common/Models/Dtos/Physical/LockerDto.cs index e7c9d975..6f44a7da 100644 --- a/src/Application/Common/Models/Dtos/Physical/LockerDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/LockerDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class LockerDto : IMapFrom +public class LockerDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public RoomDto Room { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs new file mode 100644 index 00000000..b9cc456d --- /dev/null +++ b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs @@ -0,0 +1,24 @@ +using Application.Common.Mappings; +using Application.Common.Models.Operations; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.Physical; + +public class PermissionDto : IMapFrom +{ + public bool CanRead { get; set; } + public bool CanBorrow { get; set; } + public Guid EmployeeId { get; set; } + public Guid DocumentId { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.CanRead, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(DocumentOperation.Read.ToString()))) + .ForMember(dest => dest.CanBorrow, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(DocumentOperation.Borrow.ToString()))); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs index 44fe55e0..ce9be4e2 100644 --- a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class RoomDto : IMapFrom +public class RoomDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; public string? Description { get; set; } public Guid? StaffId { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs index cc6394f4..62ec45dc 100644 --- a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class StaffDto : IMapFrom +public class StaffDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public UserDto User { get; set; } = null!; public RoomDto? Room { get; set; } diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index 3fe39e96..737b6e77 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -6,9 +6,8 @@ namespace Application.Users.Queries; -public class UserDto : IMapFrom +public class UserDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Username { get; set; } public string Email { get; set; } public string FirstName { get; set; } diff --git a/src/Application/Common/Models/Operations/DocumentOperation.cs b/src/Application/Common/Models/Operations/DocumentOperation.cs new file mode 100644 index 00000000..da6faa45 --- /dev/null +++ b/src/Application/Common/Models/Operations/DocumentOperation.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Models.Operations; + +public enum DocumentOperation +{ + Read, + Borrow, +} \ No newline at end of file diff --git a/src/Application/Departments/Commands/AddDepartment.cs b/src/Application/Departments/Commands/AddDepartment.cs index 525d83c4..a057d0d7 100644 --- a/src/Application/Departments/Commands/AddDepartment.cs +++ b/src/Application/Departments/Commands/AddDepartment.cs @@ -39,7 +39,7 @@ public async Task Handle(Command request, CancellationToken cance var entity = new Department { - Name = request.Name + Name = request.Name, }; var result = await _context.Departments.AddAsync(entity, cancellationToken); diff --git a/src/Application/Departments/Commands/UpdateDepartment.cs b/src/Application/Departments/Commands/UpdateDepartment.cs deleted file mode 100644 index a080e22d..00000000 --- a/src/Application/Departments/Commands/UpdateDepartment.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Common.Models.Dtos; -using Application.Users.Queries; -using MediatR; - -namespace Application.Departments.Commands; - -public class UpdateDepartment -{ - public record Command : IRequest - { - public Guid DepartmentId { get; set; } - public string Name { get; init; } = null!; - } -} \ No newline at end of file diff --git a/src/Application/Departments/Queries/GetDepartmentById.cs b/src/Application/Departments/Queries/GetDepartmentById.cs index 83f3f725..0073a6e0 100644 --- a/src/Application/Departments/Queries/GetDepartmentById.cs +++ b/src/Application/Departments/Queries/GetDepartmentById.cs @@ -1,6 +1,7 @@ using Application.Common.Interfaces; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using AutoMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -11,6 +12,8 @@ public class GetDepartmentById { public record Query : IRequest { + public string UserRole { get; init; } = null!; + public Guid UserDepartmentId { get; init; } public Guid DepartmentId { get; init; } } public class QueryHandler : IRequestHandler @@ -26,6 +29,12 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { + if ((request.UserRole.Equals(IdentityData.Roles.Staff) || request.UserRole.Equals(IdentityData.Roles.Employee)) + && request.UserDepartmentId != request.DepartmentId) + { + throw new UnauthorizedAccessException("User cannot access this department."); + } + var department = await _context.Departments.FirstOrDefaultAsync(x => x.Id.Equals(request.DepartmentId), cancellationToken); if (department is null) diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 903b7804..b5455f63 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,8 +1,12 @@ using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; @@ -10,6 +14,7 @@ public class DeleteDocument { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } } @@ -17,33 +22,44 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _context.Documents .Include( x => x.Folder) - .FirstOrDefaultAsync(x => x.Id.Equals(request.DocumentId), cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } - var folder = document.Folder; - var result = _context.Documents.Remove(document); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - if (folder is not null) + if (document.Folder is not null) { - folder.NumberOfDocuments -= 1; - _context.Folders.Update(folder); + document.Folder.NumberOfDocuments -= 1; + _context.Folders.Update(document.Folder); } + var log = new DocumentLog() + { + ObjectId = document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Delete, + }; + var result = _context.Documents.Remove(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index d33d4942..a60f64f2 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,11 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; @@ -13,26 +18,36 @@ public class ImportDocument { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } public Guid FolderId { get; init; } + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { + if (request.CurrentStaffRoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + var importer = await _context.Users .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); @@ -41,6 +56,16 @@ public async Task Handle(Command request, CancellationToken cancell throw new KeyNotFoundException("User does not exist."); } + if (importer.Department is null) + { + throw new ConflictException("User does not have a department."); + } + + if (importer.Department.Id != request.CurrentUser.Department!.Id) + { + throw new ConflictException("User is in another department as staff."); + } + var document = _context.Documents.FirstOrDefault(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) && x.Importer != null @@ -51,6 +76,8 @@ public async Task Handle(Command request, CancellationToken cancell } var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(y => y.Room) .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); if (folder is null) { @@ -62,6 +89,13 @@ public async Task Handle(Command request, CancellationToken cancell throw new ConflictException("This folder cannot accept more documents."); } + if (folder.Locker.Room.Id != request.CurrentStaffRoomId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new Document() { Title = request.Title.Trim(), @@ -70,12 +104,24 @@ public async Task Handle(Command request, CancellationToken cancell Importer = importer, Department = importer.Department, Folder = folder, - Status = DocumentStatus.Issued, + Status = DocumentStatus.Available, + IsPrivate = request.IsPrivate, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, + }; + var log = new DocumentLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = entity.Id, + Time = localDateTimeNow, + Action = DocumentLogMessages.Import.NewImport, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); folder.NumberOfDocuments += 1; _context.Folders.Update(folder); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs new file mode 100644 index 00000000..7b40ad74 --- /dev/null +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -0,0 +1,152 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class ShareDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + public Guid UserId { get; init; } + public bool CanRead { get; init; } + public bool CanBorrow { get; init; } + public DateTime ExpiryDate { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _applicationDbContext; + private readonly IMapper _mapper; + private readonly IPermissionManager _permissionManager; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) + { + _applicationDbContext = applicationDbContext; + _mapper = mapper; + _permissionManager = permissionManager; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await + _applicationDbContext.Documents + .Include(x => x.Importer) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.Importer!.Id != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("You are not the owner of the document."); + } + + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + if (request.ExpiryDate.ToUniversalTime() < DateTime.UtcNow) + { + throw new ConflictException("Expiry date cannot be in the past."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new DocumentLog() + { + ObjectId = document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = string.Empty, + }; + + await HandlePermissionGrantOrRevoke(request.CanRead, document, DocumentOperation.Read, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); + await HandlePermissionGrantOrRevoke(request.CanBorrow, document, DocumentOperation.Borrow, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); + + if (!string.IsNullOrEmpty(log.Action)) + { + await _applicationDbContext.DocumentLogs.AddAsync(log, cancellationToken); + } + await _applicationDbContext.SaveChangesAsync(cancellationToken); + return _mapper.Map(document); + } + + private async Task HandlePermissionGrantOrRevoke( + bool canPerformAction, + Document document, + DocumentOperation operation, + User user, + DateTime expiryDate, + CancellationToken cancellationToken, + DocumentLog log) + { + var isGranted = _permissionManager.IsGranted(document.Id, operation, user.Id); + + if (canPerformAction && !isGranted) + { + await GrantPermission(document, operation, user, expiryDate, log, cancellationToken); + } + + if (!canPerformAction && isGranted) + { + await RevokePermission(document, operation, user, log, cancellationToken); + } + } + + private async Task GrantPermission( + Document document, + DocumentOperation operation, + User user, + DateTime expiryDate, + DocumentLog log, + CancellationToken cancellationToken) + { + await _permissionManager.GrantAsync(document, operation, new[] { user }, expiryDate, cancellationToken); + + // log + log.Action = operation switch + { + DocumentOperation.Read => DocumentLogMessages.GrantRead(user.Username), + DocumentOperation.Borrow => DocumentLogMessages.GrantBorrow(user.Username), + _ => log.Action + }; + } + + private async Task RevokePermission( + Document document, + DocumentOperation operation, + User user, + DocumentLog log, + CancellationToken cancellationToken) + { + await _permissionManager.RevokeAsync(document.Id, operation, new[] { user.Id }, cancellationToken); + + // log + log.Action = operation switch + { + DocumentOperation.Read => DocumentLogMessages.RevokeRead(user.Username), + DocumentOperation.Borrow => DocumentLogMessages.RevokeBorrow(user.Username), + _ => log.Action + }; + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index d670ed86..195cc593 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -1,10 +1,16 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; @@ -31,33 +37,43 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _context.Documents + .Include(x => x.Department) .Include( x => x.Importer) - .FirstOrDefaultAsync( x => x.Id.Equals(request.DocumentId), cancellationToken); + .FirstOrDefaultAsync( x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } + + if (ViolateConstraints(request.CurrentUser, document)) + { + throw new UnauthorizedAccessException("Cannot update this document."); + } if (document.Importer is not null) { @@ -66,22 +82,41 @@ public async Task Handle(Command request, CancellationToken cancell .AnyAsync(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) && x.Id != document.Id - && x.Importer!.Id == document.Importer!.Id - , cancellationToken); + && x.Importer!.Id == document.Importer!.Id, cancellationToken); if (titleExisted) { throw new ConflictException("Document name already exists for this importer."); } } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); document.Title = request.Title; document.DocumentType = request.DocumentType; document.Description = request.Description; + document.IsPrivate = request.IsPrivate; + document.LastModified = localDateTimeNow; + document.LastModifiedBy = request.CurrentUser.Id; + var log = new DocumentLog() + { + ObjectId = document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Update, + }; var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool ViolateConstraints(User currentUser, Document document) + => (currentUser.Role.IsStaff() + && currentUser.Department!.Id != document.Department!.Id) + || (currentUser.Role.IsEmployee() + && document.ImporterId != currentUser.Id); } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs new file mode 100644 index 00000000..db36ee66 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -0,0 +1,62 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentLogsPaginated +{ + public record Query : IRequest> + { + public Guid? DocumentId { get; set; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.DocumentLogs + .Include(x => x.ObjectId) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.DocumentId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.DocumentId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.ToLower().Contains(request.SearchTerm.ToLower())); + } + + + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs new file mode 100644 index 00000000..ef5a6991 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs @@ -0,0 +1,102 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentsForEmployeePaginated +{ + public record Query : IRequest> + { + public Guid CurrentUserId { get; init; } + public Guid CurrentUserDepartmentId { get; init; } + public Guid? UserId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + public string? DocumentStatus { get; init; } + public bool IsPrivate { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents.AsQueryable(); + + documents = documents + .Include(x => x.Department) + .Include(x => x.Folder) + .ThenInclude(y => y.Locker) + .ThenInclude(z => z.Room); + + if (request.IsPrivate) + { + var permissions = _context.Permissions.Where(x => + x!.EmployeeId == request.CurrentUserId + && x.Document.Department!.Id == request.CurrentUserDepartmentId + && x.AllowedOperations.Contains(DocumentOperation.Read.ToString())) + .Select(x => x.DocumentId); + + documents = documents.Where(x => + x.Department!.Id == request.CurrentUserDepartmentId + && x.IsPrivate + && (permissions.Contains(x.Id) || x.ImporterId == request.CurrentUserId)); + } + else + { + documents = documents.Where(x => + x.Department!.Id == request.CurrentUserDepartmentId + && !x.IsPrivate); + } + + if (request.UserId is not null) + { + documents = documents.Where(x => x.Importer!.Id == request.UserId); + } + + if (request.DocumentStatus is not null + && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) + { + documents = documents.Where(x => x.Status == status); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index fd5c6a0b..40ae1e6d 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -6,6 +6,9 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -29,6 +32,9 @@ public Validator() public record Query : IRequest> { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } + public Guid? UserId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } public Guid? FolderId { get; init; } @@ -37,6 +43,9 @@ public record Query : IRequest> public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } + public string? DocumentStatus { get; init; } + public string? Role { get; init; } + public bool? IsPrivate { get; init; } } public class QueryHandler : IRequestHandler> @@ -50,9 +59,21 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Query request, - CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUser.Role.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (request.RoomId != request.CurrentStaffRoomId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + var documents = _context.Documents.AsQueryable(); var roomExists = request.RoomId is not null; var lockerExists = request.LockerId is not null; @@ -61,9 +82,29 @@ public async Task> Handle(Query request, documents = documents .Include(x => x.Department) .Include(x => x.Folder) - .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room) - .ThenInclude(t => t.Department); + .ThenInclude(y => y!.Locker) + .ThenInclude(z => z.Room); + + if (request.DocumentStatus is not null + && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) + { + documents = documents.Where(x => x.Status == status); + } + + if (request.IsPrivate is not null) + { + documents = documents.Where(x => x.IsPrivate == request.IsPrivate); + } + + if (request.UserId is not null) + { + documents = documents.Where(x => x.Importer!.Id == request.UserId); + } + + if (request.Role is not null) + { + documents = documents.Where(x => x.Importer!.Role.ToLower().Equals(request.Role.Trim().ToLower())); + } if (folderExists) { @@ -123,24 +164,14 @@ public async Task> Handle(Query request, x.Title.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(DocumentDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await documents.CountAsync(cancellationToken); - var list = await documents - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs new file mode 100644 index 00000000..207309c8 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs @@ -0,0 +1,64 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllIssuedDocumentsPaginated +{ + public record Query : IRequest> + { + public Guid DepartmentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents + .Include(x => x.Importer) + .Where(x => x.Status == DocumentStatus.Issued); + + documents = documents.Where(x => x.Department!.Id == request.DepartmentId); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index bf7c29d7..cfaa653d 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -1,6 +1,12 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using Application.Identity; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,35 +16,57 @@ public class GetDocumentById { public record Query : IRequest { - public Guid DocumentId { get; init; } + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } } public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IPermissionManager _permissionManager; - public QueryHandler(IApplicationDbContext context, IMapper mapper) + public QueryHandler( + IApplicationDbContext context, + IMapper mapper, + IPermissionManager permissionManager) { _context = context; _mapper = mapper; + _permissionManager = permissionManager; } + public async Task Handle(Query request, CancellationToken cancellationToken) { var document = await _context.Documents .Include(x => x.Department) .Include(x => x.Importer) - .Include(x => x.Folder) - .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); - + if (document is null) { throw new KeyNotFoundException("Document does not exist."); } - + + if (ViolateConstraints(request.CurrentUser, document)) + { + throw new UnauthorizedAccessException("You don't have permission to view this document."); + } + return _mapper.Map(document); } + + private bool ViolateConstraints(User user, Document document) + => IsStaffAndNotInSameDepartment(user, document) + || IsEmployeeAndDoesNotHasReadPermission(user, document); + + private static bool IsStaffAndNotInSameDepartment(User user, Document document) + => user.Role.IsStaff() + && user.Department!.Id != document.Department!.Id; + + private bool IsEmployeeAndDoesNotHasReadPermission(User user, Document document) + => user.Role.IsEmployee() + && document.ImporterId != user.Id + && !_permissionManager.IsGranted(document.Id, DocumentOperation.Read, user.Id); } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs new file mode 100644 index 00000000..d77dbf8d --- /dev/null +++ b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs @@ -0,0 +1,61 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetDocumentsOfUserPaginated +{ + public record Query : IRequest> + { + public Guid UserId { get; set; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class Handler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId + && x.IsActive + && x.IsActivated, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + var documents = _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .AsQueryable() + .Where(x => x.Importer!.Id.Equals(request.UserId) && !x.IsPrivate); + + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetPermissions.cs b/src/Application/Documents/Queries/GetPermissions.cs new file mode 100644 index 00000000..7413e097 --- /dev/null +++ b/src/Application/Documents/Queries/GetPermissions.cs @@ -0,0 +1,84 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetPermissions +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var document = await GetDocumentWithImporter(request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (IsOwner(request.CurrentUser.Id, document)) + { + return CreatePermissionDto(document.Id, request.CurrentUser.Id, true, true); + } + + var permission = await GetPermission(request.DocumentId, request.CurrentUser.Id, cancellationToken); + + if (permission is null) + { + return CreatePermissionDto(document.Id, request.CurrentUser.Id, false, false); + } + + return _mapper.Map(permission); + } + + private async Task GetDocumentWithImporter(Guid documentId, CancellationToken cancellationToken) + { + return await _context.Documents + .Include(x => x.Importer) + .FirstOrDefaultAsync(x => x.Id == documentId, cancellationToken); + } + + private static bool IsOwner(Guid userId, Document document) + { + return document.Importer!.Id == userId; + } + + private async Task GetPermission(Guid documentId, Guid employeeId, CancellationToken cancellationToken) + { + return await _context.Permissions.FirstOrDefaultAsync( + x => x!.DocumentId == documentId && x.EmployeeId == employeeId, + cancellationToken); + } + + private static PermissionDto CreatePermissionDto(Guid documentId, Guid employeeId, bool canRead, bool canBorrow) + { + return new PermissionDto() + { + DocumentId = documentId, + EmployeeId = employeeId, + CanRead = canRead, + CanBorrow = canBorrow, + }; + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs new file mode 100644 index 00000000..7c4d67cb --- /dev/null +++ b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs @@ -0,0 +1,59 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetSelfDocumentsPaginated +{ + public record Query : IRequest> + { + public Guid EmployeeId { get; init; } + public string? SearchTerm { get; set; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var documents = _context.Documents.AsQueryable(); + + documents = documents + .Include(x => x.Department) + .Where(x => x.Importer!.Id.Equals(request.EmployeeId)); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 4b7bd9b9..9eff4671 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -1,12 +1,17 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -36,6 +41,8 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -46,16 +53,20 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { - var locker = await _context.Lockers.FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); if (locker is null) { @@ -67,15 +78,20 @@ public async Task Handle(Command request, CancellationToken cancellat throw new LimitExceededException("This locker cannot accept more folders."); } - var folder = await _context.Folders.FirstOrDefaultAsync(x => - x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Locker.Id.Equals(request.LockerId), cancellationToken); + if (request.CurrentUser.Role.IsStaff() + && (locker.Room.Id != request.CurrentStaffRoomId + || !LockerIsInRoom(locker, request.CurrentStaffRoomId))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } - if (folder is not null) + if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, locker.Id, cancellationToken)) { throw new ConflictException("Folder name already exists."); } - + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new Folder { Name = request.Name.Trim(), @@ -83,13 +99,36 @@ public async Task Handle(Command request, CancellationToken cancellat NumberOfDocuments = 0, Capacity = request.Capacity, Locker = locker, - IsAvailable = true + IsAvailable = true, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, + }; + + var log = new FolderLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = entity.Id, + Time = localDateTimeNow, + Action = FolderLogMessage.Add, }; var result = await _context.Folders.AddAsync(entity, cancellationToken); locker.NumberOfFolders += 1; _context.Lockers.Update(locker); + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameFolderExistsInSameLockerAsync(string folderName, Guid lockerId, CancellationToken cancellationToken) + { + var folder = await _context.Folders.FirstOrDefaultAsync( + x => x.Name.ToLower().Equals(folderName.ToLower()) + && x.Locker.Id == lockerId, cancellationToken); + return folder is not null; + } + + private static bool LockerIsInRoom(Locker locker, Guid? roomId) + => roomId is not null && locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/DisableFolder.cs b/src/Application/Folders/Commands/DisableFolder.cs deleted file mode 100644 index f57ab34b..00000000 --- a/src/Application/Folders/Commands/DisableFolder.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands; - -public class DisableFolder -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(f => f.FolderId) - .NotEmpty().WithMessage("FolderId is required."); - } - } - - public record Command : IRequest - { - public Guid FolderId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(f => f.Id.Equals(request.FolderId), cancellationToken); - - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (!folder.IsAvailable) - { - throw new ConflictException("Folder has already been disabled."); - } - - if (folder.NumberOfDocuments > 0) - { - throw new InvalidOperationException("Folder cannot be disabled because it contains documents."); - } - - folder.IsAvailable = false; - _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(folder); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/EnableFolder.cs b/src/Application/Folders/Commands/EnableFolder.cs deleted file mode 100644 index e7acf703..00000000 --- a/src/Application/Folders/Commands/EnableFolder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands; - -public class EnableFolder -{ - public record Command : IRequest - { - public Guid FolderId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); - - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (folder.IsAvailable) - { - throw new ConflictException("Folder has already been enabled."); - } - - folder.IsAvailable = true; - var result = _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index e8bc05f2..c6a0c6c7 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,11 +1,15 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; -using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -13,6 +17,8 @@ public class RemoveFolder { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } } @@ -20,11 +26,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -40,19 +48,39 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Folder does not exist."); } - var containDocument = folder.NumberOfDocuments > 0; + if (request.CurrentUser.Role.IsStaff() + && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentStaffRoomId.Value))) + { + throw new UnauthorizedAccessException("User cannot remove this resource."); + } - if (containDocument) + var canNotRemove = folder.NumberOfDocuments > 0; + + if (canNotRemove) { throw new ConflictException("Folder cannot be removed because it contains documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var locker = folder.Locker; + + var log = new FolderLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = folder.Id, + Time = localDateTimeNow, + Action = FolderLogMessage.Remove, + }; var result = _context.Folders.Remove(folder); locker.NumberOfFolders -= 1; - + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool FolderIsInRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index edfcfe76..cec1d243 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,10 +1,16 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -31,6 +37,8 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -41,11 +49,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -60,14 +70,14 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new KeyNotFoundException("Folder does not exist."); } - - var nameExisted = await _context.Folders.AnyAsync( x => - x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Id != folder.Id - && x.Locker.Id == folder.Locker.Id - , cancellationToken); - - if (nameExisted) + + if (request.CurrentUser.Role.IsStaff() + && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentStaffRoomId!.Value))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, folder.Id, folder.Locker.Id, cancellationToken)) { throw new ConflictException("Folder name already exists."); } @@ -77,13 +87,42 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("New capacity cannot be less than current number of documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + folder.Name = request.Name; folder.Description = request.Description; folder.Capacity = request.Capacity; - + folder.LastModified = localDateTimeNow; + folder.LastModifiedBy = request.CurrentUser.Id; + + var log = new FolderLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = folder.Id, + Time = localDateTimeNow, + Action = FolderLogMessage.Update, + }; var result = _context.Folders.Update(folder); + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + private async Task DuplicatedNameFolderExistsInSameLockerAsync( + string folderName, + Guid lockerId, + Guid folderId, + CancellationToken cancellationToken) + { + var folder = await _context.Folders.FirstOrDefaultAsync( + x => x.Name.Trim().ToLower().Equals(folderName.Trim().ToLower()) + && x.Id != folderId + && x.Locker.Id == lockerId, + cancellationToken); + return folder is not null; + } + + private static bool FolderIsInRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs new file mode 100644 index 00000000..537c2dc0 --- /dev/null +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -0,0 +1,60 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Folders.Queries; + +public class GetAllFolderLogsPaginated +{ + public record Query : IRequest> + { + public Guid? FolderId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.FolderLogs + .Include(x => x.ObjectId) + .AsQueryable(); + + if (request.FolderId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.FolderId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 530d039f..d2d9d77d 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -28,6 +28,8 @@ public Validator() public record Query : IRequest> { + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } public string? SearchTerm { get; init; } @@ -50,15 +52,22 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || request.RoomId is null + || !IsSameRoom(request.RoomId.Value, request.CurrentStaffRoomId.Value))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + var folders = _context.Folders .Include(x => x.Locker) .ThenInclude(y => y.Room) .ThenInclude(z => z.Department) .AsQueryable(); - var roomExists = request.RoomId is not null; - var lockerExists = request.LockerId is not null; + var roomIdProvided = request.RoomId is not null; + var lockerIdProvided = request.LockerId is not null; - if (lockerExists) + if (lockerIdProvided) { var locker = await _context.Lockers .Include(x => x.Room) @@ -76,7 +85,7 @@ public async Task> Handle(Query request, CancellationTo folders = folders.Where(x => x.Locker.Id == request.LockerId); } - else if (roomExists) + else if (roomIdProvided) { var room = await _context.Rooms .FirstOrDefaultAsync(x => x.Id == request.RoomId @@ -95,24 +104,17 @@ public async Task> Handle(Query request, CancellationTo x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(LockerDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await folders.CountAsync(cancellationToken); - var list = await folders - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await folders + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs index c74c30c8..a2183e56 100644 --- a/src/Application/Folders/Queries/GetFolderById.cs +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -1,6 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,8 @@ public class GetFolderById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } } @@ -36,8 +40,17 @@ public async Task Handle(Query request, CancellationToken cancellatio { throw new KeyNotFoundException("Folder does not exist."); } + + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || !FolderInSameRoom(folder, request.CurrentStaffRoomId.Value))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } return _mapper.Map(folder); } + + private static bool FolderInSameRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs new file mode 100644 index 00000000..e428ad20 --- /dev/null +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -0,0 +1,137 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class ApproveOrRejectDocument +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Decision) + .Must(x => x.IsApproval() || x.IsRejection()).WithMessage("Decision is not valid."); + } + } + + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid ImportRequestId { get; init; } + public string Decision { get; init; } = null!; + public string StaffReason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId + && x.Status == ImportRequestStatus.Pending, cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == importRequest.Document.Id + && x.Status == DocumentStatus.Issued, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not assign to a room."); + } + + if (staff.Room.Id != importRequest.RoomId) + { + throw new KeyNotFoundException("Can not approve request from different room"); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new DocumentLog() + { + ObjectId = document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Import.Approve, + }; + + var requestLog = new RequestLog() + { + ObjectId = document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = RequestLogMessages.ApproveImport, + }; + + if (request.Decision.IsApproval()) + { + importRequest.Status = ImportRequestStatus.Approved; + log.Action = DocumentLogMessages.Import.Approve; + requestLog.Action = RequestLogMessages.ApproveImport; + } + + if (request.Decision.IsRejection()) + { + importRequest.Status = ImportRequestStatus.Rejected; + log.Action = DocumentLogMessages.Import.Reject; + requestLog.Action = RequestLogMessages.RejectImport; + } + + importRequest.StaffReason = request.StaffReason; + importRequest.LastModified = localDateTimeNow; + importRequest.LastModifiedBy = request.CurrentUser.Id; + + var result = _context.ImportRequests.Update(importRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs new file mode 100644 index 00000000..8b8f1fb0 --- /dev/null +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -0,0 +1,104 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class AssignDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid? StaffRoomId { get; init; } + public Guid ImportRequestId { get; init; } + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId, cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + + if (request.StaffRoomId is null || importRequest.RoomId != request.StaffRoomId.Value) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (importRequest.Status is not ImportRequestStatus.Approved) + { + throw new ConflictException("Request cannot be assigned."); + } + + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId + && x.Locker.Room.Id == request.StaffRoomId, cancellationToken); + + if (folder is null) + { + throw new ConflictException("Folder does not exist."); + } + + if (folder.NumberOfDocuments >= folder.Capacity) + { + throw new ConflictException("This folder cannot accept more documents."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + importRequest.Document.Folder = folder; + importRequest.Document.LastModified = localDateTimeNow; + importRequest.Document.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + ObjectId = importRequest.Document.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Import.Assign, + }; + var folderLog = new FolderLog() + { + ObjectId = folder.Id, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = FolderLogMessage.AssignDocument, + }; + _context.Documents.Update(importRequest.Document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.FolderLogs.AddAsync(folderLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(importRequest); + } + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs new file mode 100644 index 00000000..70c0c34a --- /dev/null +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -0,0 +1,104 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class CheckinDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.DocumentId == request.DocumentId, cancellationToken); + + if (importRequest is null) + { + throw new ConflictException("This document does not have an import request."); + } + + if (StatusesAreNotValid(document.Status, importRequest.Status)) + { + throw new ConflictException("Request cannot be checked in."); + } + + if (document.Folder is null) + { + throw new ConflictException("Request cannot be checked in."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + document.Status = DocumentStatus.Available; + document.LastModified = localDateTimeNow; + document.LastModifiedBy = request.CurrentUser.Id; + importRequest.Status = ImportRequestStatus.CheckedIn; + importRequest.LastModified = localDateTimeNow; + importRequest.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = document.Id, + Time = localDateTimeNow, + Action = DocumentLogMessages.Import.Checkin, + }; + var importLog = new RequestLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = document.Id, + Time = localDateTimeNow, + Action = RequestLogMessages.CheckInImport, + }; + var result = _context.Documents.Update(document); + _context.ImportRequests.Update(importRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(importLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + + private static bool StatusesAreNotValid(DocumentStatus documentStatus, ImportRequestStatus importRequestStatus) + => documentStatus is not DocumentStatus.Issued || importRequestStatus is not ImportRequestStatus.Approved; + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs new file mode 100644 index 00000000..d27cbeef --- /dev/null +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -0,0 +1,101 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class RequestImportDocument +{ + public record Command : IRequest + { + public string Title { get; init; } = null!; + public string? Description { get; init; } + public string DocumentType { get; init; } = null!; + public string ImportReason { get; init; } = null!; + public User Issuer { get; init; } = null!; + public Guid RoomId { get; init; } + public bool IsPrivate { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = _context.Documents.FirstOrDefault(x => + x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) + && x.Importer!.Id == request.Issuer.Id); + if (document is not null) + { + throw new ConflictException($"Document title already exists for user {request.Issuer.FirstName}."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId && x.IsAvailable, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var entity = new Document() + { + Title = request.Title.Trim(), + Description = request.Description?.Trim(), + DocumentType = request.DocumentType.Trim(), + Importer = request.Issuer, + Department = request.Issuer.Department, + Status = DocumentStatus.Issued, + IsPrivate = request.IsPrivate, + Created = localDateTimeNow, + CreatedBy = request.Issuer.Id, + }; + await _context.Documents.AddAsync(entity, cancellationToken); + + var importRequest = new ImportRequest() + { + Document = entity, + Status = ImportRequestStatus.Pending, + Room = room, + Created = localDateTimeNow, + CreatedBy = request.Issuer.Id, + ImportReason = request.ImportReason, + StaffReason = string.Empty, + }; + + var log = new DocumentLog() + { + ObjectId = entity.Id, + Time = localDateTimeNow, + User = request.Issuer, + UserId = request.Issuer.Id, + Action = DocumentLogMessages.Import.NewImportRequest, + }; + var result = await _context.ImportRequests.AddAsync(importRequest, cancellationToken); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs new file mode 100644 index 00000000..f90d4991 --- /dev/null +++ b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs @@ -0,0 +1,106 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.ImportRequests.Queries; + +public class GetAllImportRequestsPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public Guid? RoomId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + if (request.CurrentUser.Role.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + + if (request.CurrentUser.Role.IsEmployee()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + + var importRequests = _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.RoomId is not null) + { + importRequests = importRequests.Where(x => x.RoomId == request.RoomId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + importRequests = importRequests.Where(x => + x.Document.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + + return await importRequests + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + + private static bool RoomIsNotInSameDepartment(User user, Room room) + => user.Department?.Id != room.DepartmentId; + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Queries/GetImportRequestById.cs b/src/Application/ImportRequests/Queries/GetImportRequestById.cs new file mode 100644 index 00000000..625400cd --- /dev/null +++ b/src/Application/ImportRequests/Queries/GetImportRequestById.cs @@ -0,0 +1,57 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.ImportRequests.Queries; + +public class GetImportRequestById { + public record Query : IRequest + { + public Guid CurrentUserId { get; init; } + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } + public Guid RequestId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id.Equals(request.RequestId), cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || importRequest.Room.Id != request.CurrentStaffRoomId)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (request.CurrentUserRole.IsEmployee() + && importRequest.Document.ImporterId != request.CurrentUserId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + return _mapper.Map(importRequest); + } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 78cbb371..f1a9097c 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -1,12 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Lockers.Commands; @@ -35,6 +39,7 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public string Name { get; init; } = null!; public string? Description { get; init; } public Guid RoomId { get; init; } @@ -45,11 +50,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -63,34 +70,50 @@ public async Task Handle(Command request, CancellationToken cancellat if (room.NumberOfLockers >= room.Capacity) { - throw new LimitExceededException( - "This room cannot accept more lockers." - ); + throw new LimitExceededException("This room cannot accept more lockers."); } - var locker = await _context.Lockers.FirstOrDefaultAsync( - x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Room.Id.Equals(request.RoomId), - cancellationToken); - if (locker is not null) + if (await DuplicatedNameLockerExistsInSameRoomAsync(request.Name, request.RoomId, cancellationToken)) { throw new ConflictException("Locker name already exists."); } - var entity = new Locker + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var entity = new Locker() { Name = request.Name.Trim(), Description = request.Description?.Trim(), NumberOfFolders = 0, Capacity = request.Capacity, Room = room, - IsAvailable = true + IsAvailable = true, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, + }; + + var log = new LockerLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = entity.Id, + Time = localDateTimeNow, + Action = LockerLogMessage.Add, }; - var result = await _context.Lockers.AddAsync(entity, cancellationToken); room.NumberOfLockers += 1; _context.Rooms.Update(room); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameLockerExistsInSameRoomAsync(string lockerName, Guid roomId, CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => x.Name.ToLower().Equals(lockerName.ToLower()) + && x.Room.Id == roomId, cancellationToken); + return locker is not null; + } } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/DisableLocker.cs b/src/Application/Lockers/Commands/DisableLocker.cs deleted file mode 100644 index 10a78f79..00000000 --- a/src/Application/Lockers/Commands/DisableLocker.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands; - -public class DisableLocker -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("LockerId is required."); - } - } - - public record Command : IRequest - { - public Guid LockerId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (!locker.IsAvailable) - { - throw new ConflictException("Locker has already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Locker cannot be disabled because it contains documents."); - } - - var folders = _context.Folders.Where(x => x.Locker.Room.Id.Equals(locker.Id)); - - foreach (var folder in folders) - { - folder.IsAvailable = false; - } - _context.Folders.UpdateRange(folders); - - locker.IsAvailable = false; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/EnableLocker.cs b/src/Application/Lockers/Commands/EnableLocker.cs deleted file mode 100644 index 49722a1a..00000000 --- a/src/Application/Lockers/Commands/EnableLocker.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands; - -public class EnableLocker -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("LockerId is required."); - } - } - - public record Command : IRequest - { - public Guid LockerId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.IsAvailable) - { - throw new ConflictException("Locker has already been enabled."); - } - - locker.IsAvailable = true; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index a6ab6ad9..c58f35ab 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,10 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Lockers.Commands; @@ -23,6 +27,7 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid LockerId { get; init; } } @@ -30,11 +35,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -43,26 +50,34 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Room) .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - + if (locker is null) { throw new KeyNotFoundException("Locker does not exist."); } - + var canNotRemove = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) - > 0; - + .AnyAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken); if (canNotRemove) { - throw new InvalidOperationException("Locker cannot be removed because it contains documents."); + throw new ConflictException("Locker cannot be removed because it contains documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new LockerLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = locker.Id, + Time = localDateTimeNow, + Action = LockerLogMessage.Remove, + }; var room = locker.Room; - var result = _context.Lockers.Remove(locker); room.NumberOfLockers -= 1; _context.Rooms.Update(room); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 20383ef9..e618129a 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,12 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Lockers.Commands; @@ -31,6 +35,7 @@ public Validator() } public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid LockerId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -41,11 +46,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -54,34 +61,57 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Room) .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - + if (locker is null) { throw new KeyNotFoundException("Locker does not exist."); } - - var duplicateLocker = await _context.Lockers.FirstOrDefaultAsync( - x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Id != locker.Id - && x.Room.Id == locker.Room.Id, cancellationToken); - if (duplicateLocker is not null && !duplicateLocker.Equals(locker)) + if (await DuplicatedNameLockerExistsInSameRoomAsync(request.Name, locker.Room.Id, request.LockerId, cancellationToken)) { throw new ConflictException("New locker name already exists."); } - + if (locker.NumberOfFolders > request.Capacity) { throw new ConflictException("New capacity cannot be less than current number of folders."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + // update work locker.Name = request.Name; locker.Description = request.Description; locker.Capacity = request.Capacity; - + locker.LastModified = localDateTimeNow; + locker.LastModifiedBy = request.CurrentUser.Id; + + var log = new LockerLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = locker.Id, + Time = localDateTimeNow, + Action = LockerLogMessage.Update, + }; var result = _context.Lockers.Update(locker); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameLockerExistsInSameRoomAsync( + string lockerName, + Guid roomId, + Guid lockerId, + CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => x.Name.Trim().ToLower().Equals(lockerName.ToLower()) + && x.Id != lockerId + && x.Room.Id == roomId, + cancellationToken); + return locker is not null; + } } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs new file mode 100644 index 00000000..6adb27be --- /dev/null +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -0,0 +1,93 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Queries; + +public class GetAllLockerLogsPaginated +{ + public record Query : IRequest> + { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } + public string? SearchTerm { get; init; } + public Guid? LockerId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + + if (request.CurrentUserRole.IsStaff()) + { + if (request.LockerId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var currentRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + + if (currentRoom is null) + { + throw new UnauthorizedAccessException("User cannot access this resource"); + } + + if (!IsSameRoom(currentRoom.Id, request.LockerId.Value)) + { + throw new UnauthorizedAccessException("User cannot access this resource"); + } + } + + var logs = _context.LockerLogs + .Include(x => x.ObjectId) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.LockerId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.LockerId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); + } + + private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) + => await _context.Rooms.FirstOrDefaultAsync( + x => x.DepartmentId == departmentId, + cancellationToken); + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; + } +} diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 3e08e523..ad6b845c 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Mappings; @@ -5,6 +6,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -14,6 +16,8 @@ public class GetAllLockersPaginated { public record Query : IRequest> { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid? RoomId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } @@ -35,6 +39,26 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUserRole.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var currentUserRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + + if (currentUserRoom is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (!IsSameRoom(currentUserRoom.Id, request.RoomId.Value)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + var lockers = _context.Lockers .Include(x => x.Room) .ThenInclude(y => y.Department) @@ -51,24 +75,22 @@ public async Task> Handle(Query request, CancellationTo x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(LockerDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + return await lockers + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } - var count = await lockers.CountAsync(cancellationToken); - var list = await lockers - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); + private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) + => await _context.Rooms.FirstOrDefaultAsync( + x => x.DepartmentId == departmentId, + cancellationToken); - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); - } + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 64a1bf5a..3a21a7a4 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -1,6 +1,9 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +13,8 @@ public class GetLockerById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid LockerId { get; init; } } @@ -35,8 +40,19 @@ public async Task Handle(Query request, CancellationToken cancellatio { throw new KeyNotFoundException("Locker does not exist."); } - + + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || !LockerInSameRoom(locker, request.CurrentStaffRoomId.Value))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + return _mapper.Map(locker); } + + private static bool LockerInSameRoom( + Locker locker, + Guid roomId) + => locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index 13449b93..031b1131 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -1,11 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -41,6 +45,7 @@ private bool BeUnique(string name) public record Command : IRequest { + public User CurrentUser { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -51,10 +56,12 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -75,6 +82,7 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("Room name already exists."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var entity = new Room { Name = request.Name.Trim(), @@ -84,8 +92,20 @@ public async Task Handle(Command request, CancellationToken cancellatio Department = department, DepartmentId = request.DepartmentId, IsAvailable = true, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, + }; + + var log = new RoomLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = entity.Id, + Time = localDateTimeNow, + Action = RoomLogMessage.Add, }; var result = await _context.Rooms.AddAsync(entity, cancellationToken); + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/DisableRoom.cs b/src/Application/Rooms/Commands/DisableRoom.cs deleted file mode 100644 index c70fe9a5..00000000 --- a/src/Application/Rooms/Commands/DisableRoom.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands; - -public class DisableRoom -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } - } - - public record Command : IRequest - { - public Guid RoomId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .Include(x => x.Department) - .Include(x => x.Staff) - .Include(x => x.Lockers) - .ThenInclude(y => y.Folders) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (!room.IsAvailable) - { - throw new ConflictException("Room have already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Room cannot be disabled because it contains documents."); - } - - var lockers = _context.Lockers.Include(x=> x.Folders) - .Where(x => x.Room.Id.Equals(room.Id)); - - foreach (var locker in lockers) - { - foreach (var folder in locker.Folders) - { - folder.IsAvailable = false; - } - locker.IsAvailable = false; - } - _context.Lockers.UpdateRange(lockers); - room.IsAvailable = false; - var result = _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/EnableRoom.cs b/src/Application/Rooms/Commands/EnableRoom.cs deleted file mode 100644 index d27e9faf..00000000 --- a/src/Application/Rooms/Commands/EnableRoom.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands; - -public class EnableRoom -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } - } - public record Command : IRequest - { - public Guid RoomId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .Include(x => x.Department) - .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (room.IsAvailable) - { - throw new ConflictException("Room has already been enabled."); - } - - room.IsAvailable = true; - var result = _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index b389e8d6..6f56a584 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,9 +1,14 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -22,6 +27,7 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } } @@ -29,34 +35,49 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { var room = await _context.Rooms .Include(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken); + if (room is null) { throw new KeyNotFoundException("Room does not exist."); } - var canNotRemove = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken) - > 0; - - if (canNotRemove) + var containsDocuments = await _context.Documents + .AnyAsync(x => x.Folder!.Locker.Room.Id == room.Id, cancellationToken); + var containsFolders = await _context.Folders + .AnyAsync(x => x.Locker.Room.Id == room.Id, cancellationToken); + var containsLockers = await _context.Lockers + .AnyAsync(x => x.Room.Id == room.Id, cancellationToken); + if (containsDocuments || containsFolders || containsLockers) { - throw new InvalidOperationException("Room cannot be removed because it contains documents."); + throw new ConflictException("Room cannot be removed because it contains something."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new RoomLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = room.Id, + Time = localDateTimeNow, + Action = RoomLogMessage.Remove, + }; var result = _context.Rooms.Remove(room); + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index a4fbe702..d585ae1d 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,11 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -30,21 +34,25 @@ public Validator() } public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } + public bool IsAvailable { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -52,6 +60,7 @@ public async Task Handle(Command request, CancellationToken cancellatio var room = await _context.Rooms .Include(x => x.Department) .Include(x => x.Staff) + .AsNoTracking() .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) @@ -59,12 +68,7 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Room does not exist."); } - var nameExisted = await _context.Rooms.AnyAsync(x => x.Name - .ToLower().Equals(request.Name.ToLower()) - && x.Id != room.Id - , cancellationToken: cancellationToken); - - if (nameExisted) + if (await DuplicatedNameRoomExistsAsync(request.Name, request.RoomId, cancellationToken)) { throw new ConflictException("Name has already exists."); } @@ -74,26 +78,40 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("New capacity cannot be less than current number of lockers."); } - var updatedRoom = new Room - { - Id = room.Id, - Name = request.Name, - Description = request.Description, - Staff = room.Staff, - Department = room.Department, - DepartmentId = room.DepartmentId, - Capacity = request.Capacity, - NumberOfLockers = room.NumberOfLockers, - IsAvailable = room.IsAvailable, - Lockers = room.Lockers - }; + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - _context.Rooms.Entry(room).State = EntityState.Detached; - _context.Rooms.Entry(updatedRoom).State = EntityState.Modified; + // update work + room.Name = request.Name; + room.Description = request.Description; + room.Capacity = request.Capacity; + room.IsAvailable = request.IsAvailable; + room.LastModified = localDateTimeNow; + room.LastModifiedBy = request.CurrentUser.Id; + var log = new RoomLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = room.Id, + Time = localDateTimeNow, + Action = RoomLogMessage.Update, + }; + _context.Rooms.Entry(room).State = EntityState.Modified; + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - - return _mapper.Map(updatedRoom); + return _mapper.Map(room); + } + + private async Task DuplicatedNameRoomExistsAsync( + string roomName, + Guid roomId, + CancellationToken cancellationToken) + { + var room = await _context.Rooms.FirstOrDefaultAsync( + x => x.Name.Trim().ToLower().Equals(roomName.Trim().ToLower()) + && x.Id != roomId, + cancellationToken); + return room is not null; } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs new file mode 100644 index 00000000..37b9f7ff --- /dev/null +++ b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs @@ -0,0 +1,61 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetAllRoomLogsPaginated +{ + public record Query : IRequest> + { + public Guid? RoomId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.RoomLogs + .Include(x => x.ObjectId) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.RoomId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.RoomId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.ToLower().Contains(request.SearchTerm.ToLower())); + } + + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index ff89fe2b..b2961b8b 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -5,6 +5,8 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -14,6 +16,8 @@ public class GetAllRoomsPaginated { public record Query : IRequest> { + public User CurrentUser { get; init; } = null!; + public Guid? DepartmentId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -39,30 +43,31 @@ public async Task> Handle(Query request, CancellationToke .Include(x => x.Staff) .AsQueryable(); - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + if ((request.CurrentUser.Role.IsStaff() || request.CurrentUser.Role.IsEmployee()) + && request.CurrentUser.Department?.Id != request.DepartmentId) { - rooms = rooms.Where(x => - x.Name.ToLower().Contains(request.SearchTerm.ToLower())); + throw new UnauthorizedAccessException("User cannot access this resource."); } - - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) + + if (request.DepartmentId is not null) { - sortBy = nameof(RoomDto.Id); + rooms = rooms.Where(x => x.Department.Id == request.DepartmentId); } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - var count = await rooms.CountAsync(cancellationToken); - var list = await rooms - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + rooms = rooms.Where(x => + x.Name.ToLower().Contains(request.SearchTerm.ToLower())); + } - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await rooms + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs index 3534dacb..96157358 100644 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs +++ b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs @@ -44,7 +44,7 @@ public async Task> Handle(Query request, Cancellat .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable() .ToList(); - + lockers.ForEach(x => x.Folders = x.Folders.Where(y => y.Slot > 0)); var result = new PaginatedList(lockers.ToList(), lockers.Count, pageNumber, sizeNumber); diff --git a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs new file mode 100644 index 00000000..45e99e75 --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetRoomByDepartmentId +{ + public record Query : IRequest + { + public Guid DepartmentId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) + .FirstOrDefaultAsync(x => x.DepartmentId == request.DepartmentId, cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + return _mapper.Map(room); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 2d420d10..76532fbe 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -1,3 +1,4 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -11,6 +12,8 @@ public class GetRoomById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid RoomId { get; init; } } @@ -30,14 +33,23 @@ public async Task Handle(Query request, CancellationToken cancellationT var room = await _context.Rooms .Include(x => x.Department) .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken: cancellationToken); + if (room is null) { throw new KeyNotFoundException("Room does not exist."); } - + + if ((request.CurrentUserRole.IsStaff() || request.CurrentUserRole.IsEmployee()) + && !IsSameDepartment(request.CurrentUserDepartmentId, room.DepartmentId)) + { + throw new UnauthorizedAccessException("User cannot update this resource."); + } + return _mapper.Map(room); } + + private static bool IsSameDepartment(Guid departmentId1, Guid departmentId2) + => departmentId1 == departmentId2; } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomByStaffId.cs b/src/Application/Rooms/Queries/GetRoomByStaffId.cs new file mode 100644 index 00000000..7a0c4f90 --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomByStaffId.cs @@ -0,0 +1,51 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetRoomByStaffId +{ + public record Query : IRequest + { + public Guid StaffId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .FirstOrDefaultAsync(x => x.Id == request.StaffId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exists."); + } + + var room = await _context.Rooms + .Include(x => x.Staff) + .ThenInclude(y => y!.User) + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Staff!.Id == request.StaffId, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exists."); + } + + return _mapper.Map(room); + } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs deleted file mode 100644 index 0d78536f..00000000 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Staffs.Commands; - -public class AddStaff -{ - public record Command : IRequest - { - public Guid UserId { get; init; } - public Guid? RoomId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - var existedStaff = await _context.Staffs - .Include(x => x.Room) - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken); - if (existedStaff is not null) - { - if (existedStaff.Room is not null) - { - throw new ConflictException("This user is already a staff."); - } - - existedStaff.Room = room; - var result = _context.Staffs.Update(existedStaff); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - else - { - var staff = new Staff - { - Id = user.Id, - User = user, - Room = room - }; - - var result = await _context.Staffs.AddAsync(staff, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } - } -} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AssignStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs new file mode 100644 index 00000000..3e569701 --- /dev/null +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -0,0 +1,90 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Staffs.Commands; + +public class AssignStaff +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid StaffId { get; init; } + public Guid? RoomId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Id == request.StaffId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + var room = await _context.Rooms + .Include(x => x.Staff) + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + if (!room.IsAvailable) + { + throw new ConflictException("Room is not available."); + } + + if (room.Staff is not null) + { + throw new ConflictException("Room already has a staff."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + staff.Room = room; + room.Staff = staff; + room.LastModified = localDateTimeNow; + room.LastModifiedBy = request.CurrentUser.Id; + + var log = new UserLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = staff.User.Id, + Time = localDateTimeNow, + Action = UserLogMessages.Staff.AssignStaff(room.Id.ToString()), + }; + _context.Rooms.Update(room); + var result = _context.Staffs.Update(staff); + await _context.UserLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + + } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaff.cs b/src/Application/Staffs/Commands/RemoveStaff.cs deleted file mode 100644 index d550094b..00000000 --- a/src/Application/Staffs/Commands/RemoveStaff.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Staffs.Commands; - -public class RemoveStaff -{ - public record Command : IRequest - { - public Guid StaffId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var staff = await _context.Staffs - .Include(x => x.User) - .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.User.Id.Equals(request.StaffId), cancellationToken: cancellationToken); - - if (staff is null) - { - throw new KeyNotFoundException("Staff does not exist."); - } - - var result = _context.Staffs.Remove(staff); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index b4e8be49..448c2093 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,9 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Staffs.Commands; @@ -11,6 +15,7 @@ public class RemoveStaffFromRoom { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid StaffId { get; init; } } @@ -18,11 +23,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -42,10 +49,22 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("Staff is not assigned to a room."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + staff.Room.Staff = null; _context.Rooms.Update(staff.Room!); staff.Room = null; + + var log = new UserLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = staff.User.Id, + Time = localDateTimeNow, + Action = UserLogMessages.Staff.RemoveFromRoom, + }; var result = _context.Staffs.Update(staff); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs b/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs new file mode 100644 index 00000000..2a8baf29 --- /dev/null +++ b/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs @@ -0,0 +1,28 @@ +using Application.Common.Interfaces; +using Domain.Entities.Physical; +using Domain.Events; +using MediatR; + +namespace Application.Staffs.EventHandlers; + +public class StaffCreatedEventHandler : INotificationHandler +{ + private readonly IApplicationDbContext _context; + + public StaffCreatedEventHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(StaffCreatedEvent notification, CancellationToken cancellationToken) + { + var staff = new Staff() + { + Id = notification.Staff.Id, + User = notification.Staff, + }; + + await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs index 61f62046..c0987de1 100644 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -45,25 +46,14 @@ public async Task> Handle(Query request, CancellationTok staffs = staffs.Where(x => x.User.Username.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(StaffDto.Id); - } - - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await staffs.CountAsync(cancellationToken); - var list = await staffs - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value,sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await staffs + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetStaffByRoom.cs b/src/Application/Staffs/Queries/GetStaffByRoomId.cs similarity index 77% rename from src/Application/Staffs/Queries/GetStaffByRoom.cs rename to src/Application/Staffs/Queries/GetStaffByRoomId.cs index 60db1c83..119b96f1 100644 --- a/src/Application/Staffs/Queries/GetStaffByRoom.cs +++ b/src/Application/Staffs/Queries/GetStaffByRoomId.cs @@ -1,15 +1,18 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Staffs.Queries; -public class GetStaffByRoom +public class GetStaffByRoomId { public record Query : IRequest { + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } } @@ -37,6 +40,12 @@ public async Task Handle(Query request, CancellationToken cancellation throw new KeyNotFoundException("Room does not exist."); } + if (request.CurrentUser.Role.IsStaff() + && room.DepartmentId != request.CurrentUser.Department!.Id) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + if (room.Staff is null) { throw new KeyNotFoundException("Staff does not exist."); diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 2ccef831..38c94551 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Helpers; using Application.Identity; using Application.Users.Queries; using AutoMapper; using Domain.Entities; +using Domain.Entities.Logging; using Domain.Events; using FluentValidation; using MediatR; @@ -53,6 +56,7 @@ private static bool BeNotAdmin(string role) public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public string Username { get; init; } = null!; public string Email { get; init; } = null!; public string? FirstName { get; init; } @@ -67,16 +71,23 @@ public class AddUserCommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly ISecurityService _securityService; + private readonly IDateTimeProvider _dateTimeProvider; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService) + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; _securityService = securityService; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { + if (request.Role.IsAdmin()) + { + throw new UnauthorizedAccessException(); + } + var user = await _context.Users.FirstOrDefaultAsync( x => x.Username.Equals(request.Username) || x.Email.Equals(request.Email), cancellationToken); @@ -96,6 +107,8 @@ public async Task Handle(Command request, CancellationToken cancellatio var password = StringUtil.RandomPassword(); var salt = StringUtil.RandomSalt(); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new User { Username = request.Username, @@ -109,10 +122,27 @@ public async Task Handle(Command request, CancellationToken cancellatio Position = request.Position, IsActive = true, IsActivated = false, - Created = LocalDateTime.FromDateTime(DateTime.UtcNow) + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, }; + + entity.AddDomainEvent(new UserCreatedEvent(entity, password)); + if (request.Role.IsStaff()) + { + entity.AddDomainEvent(new StaffCreatedEvent(entity, request.CurrentUser)); + } var result = await _context.Users.AddAsync(entity, cancellationToken); + + var log = new UserLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = entity.Id, + Time = localDateTimeNow, + Action = UserLogMessages.Add(entity.Role), + }; + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Users/Commands/DisableUser.cs b/src/Application/Users/Commands/DisableUser.cs deleted file mode 100644 index dafb4b31..00000000 --- a/src/Application/Users/Commands/DisableUser.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Users.Queries; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Users.Commands; - -public class DisableUser -{ - public record Command : IRequest - { - public Guid UserId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - if (!user.IsActive) - { - throw new ConflictException("User has already been disabled."); - } - - user.IsActive = false; - - var result = _context.Users.Update(user); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/EnableUser.cs b/src/Application/Users/Commands/EnableUser.cs deleted file mode 100644 index 7da8ab48..00000000 --- a/src/Application/Users/Commands/EnableUser.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Users.Commands; - -public class EnableUser -{ - public record Command : IRequest - { - public Guid UserId { get; init; } - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 00b27531..8cb22bcd 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,9 +1,15 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Identity; using Application.Users.Queries; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Users.Commands; @@ -27,25 +33,36 @@ public Validator() } public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid UserId { get; init; } public string? FirstName { get; init; } public string? LastName { get; init; } public string? Position { get; init; } - } + public string Role { get; init; } = null!; + public bool IsActive { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { + // save a roundtrip to db + if (request.CurrentUser.Role.IsAdmin() + && UpdateSelf(request.CurrentUser.Id, request.UserId)) + { + throw new UnauthorizedAccessException("User cannot update this resource."); + } + var user = await _context.Users .FirstOrDefaultAsync(x => x.Id.Equals(request.UserId), cancellationToken: cancellationToken); @@ -54,13 +71,31 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("User does not exist."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + user.FirstName = request.FirstName; user.LastName = request.LastName; user.Position = request.Position; - + user.Role = request.Role; + user.IsActive = request.IsActive; + user.LastModified = localDateTimeNow; + user.LastModifiedBy = request.CurrentUser.Id; + + var log = new UserLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = user.Id, + Time = localDateTimeNow, + Action = UserLogMessages.Update, + }; var result = _context.Users.Update(user); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool UpdateSelf(Guid currentUserId, Guid updatingUserId) + => updatingUserId == currentUserId; } } \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs new file mode 100644 index 00000000..ee1c3e7c --- /dev/null +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -0,0 +1,61 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Queries; + +public class GetAllUserLogsPaginated +{ + public record Query : IRequest> + { + public Guid? UserId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.UserLogs + .Include(x => x.ObjectId) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.UserId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.UserId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index f8613324..27d57746 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -1,13 +1,10 @@ using Application.Common.Extensions; using Application.Common.Interfaces; -using Application.Common.Mappings; using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; using Application.Identity; using AutoMapper; -using AutoMapper.QueryableExtensions; +using Domain.Entities; using MediatR; -using Microsoft.EntityFrameworkCore; namespace Application.Users.Queries; @@ -15,7 +12,8 @@ public class GetAllUsersPaginated { public record Query : IRequest> { - public Guid? DepartmentId { get; init; } + public Guid[]? DepartmentIds { get; init; } + public string? Role { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -36,38 +34,36 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var users = _context.Users.AsQueryable() + var users = _context.Users .Where(x => !x.Role.Equals(IdentityData.Roles.Admin)); - if (request.DepartmentId is not null) + // Filter by department + if (request.DepartmentIds is not null) { - users = users.Where(x => x.Department!.Id == request.DepartmentId); + users = users.Where(x => request.DepartmentIds.Contains(x.Department!.Id) ); + } + + // Filter by role + if (request.Role is not null) + { + users = users.Where(x => x.Role.ToLower().Equals(request.Role.Trim().ToLower())); } + // Search if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { users = users.Where(x => x.FirstName!.ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(UserDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await users.CountAsync(cancellationToken); - var list = await users - .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await users + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs index 7b22fe51..d49ddb29 100644 --- a/src/Application/Users/Queries/GetUserById.cs +++ b/src/Application/Users/Queries/GetUserById.cs @@ -1,5 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Identity; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -9,9 +12,11 @@ public class GetUserById { public record Query : IRequest { + public string UserRole { get; init; } = null!; + public Guid UserDepartmentId { get; init; } public Guid UserId { get; init; } } - + public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; @@ -34,7 +39,16 @@ public async Task Handle(Query request, CancellationToken cancellationT throw new KeyNotFoundException("User does not exist."); } + if (ViolateConstraints(request.UserRole, request.UserDepartmentId, user)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + return _mapper.Map(user); } + + private static bool ViolateConstraints(string userRole, Guid userDepartmentId, User foundUser) + => (userRole.IsStaff() || userRole.IsEmployee()) + && userDepartmentId != foundUser.Department?.Id; } } \ No newline at end of file diff --git a/src/Domain/Common/BaseLoggingEntity.cs b/src/Domain/Common/BaseLoggingEntity.cs new file mode 100644 index 00000000..d7379232 --- /dev/null +++ b/src/Domain/Common/BaseLoggingEntity.cs @@ -0,0 +1,14 @@ +using Domain.Entities; +using NodaTime; + +namespace Domain.Common; + +public class BaseLoggingEntity : BaseEntity +{ + public string Action { get; set; } = null!; + public Guid UserId { get; set; } + public Guid? ObjectId { get; set; } + public LocalDateTime Time { get; set; } + + public User User { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Department.cs b/src/Domain/Entities/Department.cs index b43bbfe7..6a353b38 100644 --- a/src/Domain/Entities/Department.cs +++ b/src/Domain/Entities/Department.cs @@ -6,5 +6,6 @@ namespace Domain.Entities; public class Department : BaseEntity { public string Name { get; set; } = null!; - public Room? Room { get; set; } + + public ICollection Rooms { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/DocumentLog.cs b/src/Domain/Entities/Logging/DocumentLog.cs new file mode 100644 index 00000000..e52fae0c --- /dev/null +++ b/src/Domain/Entities/Logging/DocumentLog.cs @@ -0,0 +1,9 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class DocumentLog : BaseLoggingEntity +{ + public Folder? BaseFolder { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/FolderLog.cs b/src/Domain/Entities/Logging/FolderLog.cs new file mode 100644 index 00000000..e41cbdf2 --- /dev/null +++ b/src/Domain/Entities/Logging/FolderLog.cs @@ -0,0 +1,9 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class FolderLog : BaseLoggingEntity +{ + public Locker? BaseLocker { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/LockerLog.cs b/src/Domain/Entities/Logging/LockerLog.cs new file mode 100644 index 00000000..96c8ea44 --- /dev/null +++ b/src/Domain/Entities/Logging/LockerLog.cs @@ -0,0 +1,9 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class LockerLog : BaseLoggingEntity +{ + public Room? BaseRoom { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs new file mode 100644 index 00000000..5e801092 --- /dev/null +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -0,0 +1,10 @@ +using Domain.Common; +using Domain.Entities.Physical; +using Domain.Enums; + +namespace Domain.Entities.Logging; + +public class RequestLog : BaseLoggingEntity +{ + public RequestType Type { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RoomLog.cs b/src/Domain/Entities/Logging/RoomLog.cs new file mode 100644 index 00000000..b662a46f --- /dev/null +++ b/src/Domain/Entities/Logging/RoomLog.cs @@ -0,0 +1,8 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class RoomLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/UserLog.cs b/src/Domain/Entities/Logging/UserLog.cs new file mode 100644 index 00000000..32735697 --- /dev/null +++ b/src/Domain/Entities/Logging/UserLog.cs @@ -0,0 +1,7 @@ +using Domain.Common; + +namespace Domain.Entities.Logging; + +public class UserLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index 8b3076ac..bd05231a 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -4,13 +4,14 @@ namespace Domain.Entities.Physical; -public class Borrow : BaseEntity +public class Borrow : BaseAuditableEntity { public User Borrower { get; set; } = null!; public Document Document { get; set; } = null!; public LocalDateTime BorrowTime { get; set; } public LocalDateTime DueTime { get; set; } public LocalDateTime ActualReturnTime { get; set; } - public string Reason { get; set; } = null!; + public string BorrowReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public BorrowRequestStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index a473a2b5..0de04af1 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -4,16 +4,18 @@ namespace Domain.Entities.Physical; -public class Document : BaseEntity +public class Document : BaseAuditableEntity { public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; + public Guid? ImporterId { get; set; } public Department? Department { get; set; } - public User? Importer { get; set; } public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } public Guid? EntryId { get; set; } + public bool IsPrivate { get; set; } + public User? Importer { get; set; } public virtual Entry? Entry { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Folder.cs b/src/Domain/Entities/Physical/Folder.cs index 596e648c..bb7aacee 100644 --- a/src/Domain/Entities/Physical/Folder.cs +++ b/src/Domain/Entities/Physical/Folder.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Folder : BaseEntity +public class Folder : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Entities/Physical/ImportRequest.cs b/src/Domain/Entities/Physical/ImportRequest.cs new file mode 100644 index 00000000..b5060d88 --- /dev/null +++ b/src/Domain/Entities/Physical/ImportRequest.cs @@ -0,0 +1,16 @@ +using Domain.Common; +using Domain.Statuses; + +namespace Domain.Entities.Physical; + +public class ImportRequest : BaseAuditableEntity +{ + public Guid RoomId { get; set; } + public Guid DocumentId { get; set; } + public string ImportReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; + public ImportRequestStatus Status { get; set; } + + public Room Room { get; set; } = null!; + public Document Document { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Locker.cs b/src/Domain/Entities/Physical/Locker.cs index 5f0af84c..3bd9f836 100644 --- a/src/Domain/Entities/Physical/Locker.cs +++ b/src/Domain/Entities/Physical/Locker.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Locker : BaseEntity +public class Locker : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Entities/Physical/Permission.cs b/src/Domain/Entities/Physical/Permission.cs new file mode 100644 index 00000000..29b01181 --- /dev/null +++ b/src/Domain/Entities/Physical/Permission.cs @@ -0,0 +1,14 @@ +using NodaTime; + +namespace Domain.Entities.Physical; + +public class Permission +{ + public Guid EmployeeId { get; set; } + public Guid DocumentId { get; set; } + public string AllowedOperations { get; set; } = null!; + public LocalDateTime ExpiryDateTime { get; set; } + + public User Employee { get; set; } = null!; + public Document Document { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Room.cs b/src/Domain/Entities/Physical/Room.cs index 3d2318ac..e6b988db 100644 --- a/src/Domain/Entities/Physical/Room.cs +++ b/src/Domain/Entities/Physical/Room.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Room : BaseEntity +public class Room : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Enums/RequestType.cs b/src/Domain/Enums/RequestType.cs new file mode 100644 index 00000000..f6dbac41 --- /dev/null +++ b/src/Domain/Enums/RequestType.cs @@ -0,0 +1,7 @@ +namespace Domain.Enums; + +public enum RequestType +{ + Import, + Borrow, +} \ No newline at end of file diff --git a/src/Domain/Events/StaffCreatedEvent.cs b/src/Domain/Events/StaffCreatedEvent.cs new file mode 100644 index 00000000..2b6adfb1 --- /dev/null +++ b/src/Domain/Events/StaffCreatedEvent.cs @@ -0,0 +1,16 @@ +using Domain.Common; +using Domain.Entities; + +namespace Domain.Events; + +public class StaffCreatedEvent : BaseEvent +{ + public StaffCreatedEvent(User staff, User currentUser) + { + Staff = staff; + CurrentUser = currentUser; + } + + public User Staff { get; } + public User CurrentUser { get; } +} \ No newline at end of file diff --git a/src/Domain/Statuses/BorrowRequestStatus.cs b/src/Domain/Statuses/BorrowRequestStatus.cs index 15c19a0b..8ef13936 100644 --- a/src/Domain/Statuses/BorrowRequestStatus.cs +++ b/src/Domain/Statuses/BorrowRequestStatus.cs @@ -2,13 +2,13 @@ namespace Domain.Statuses; public enum BorrowRequestStatus { - Approved, Pending, + Approved, Rejected, - Overdue, - Cancelled, CheckedOut, Returned, + Overdue, + Cancelled, Lost, NotProcessable, } \ No newline at end of file diff --git a/src/Domain/Statuses/ImportRequestStatus.cs b/src/Domain/Statuses/ImportRequestStatus.cs new file mode 100644 index 00000000..65a65ffc --- /dev/null +++ b/src/Domain/Statuses/ImportRequestStatus.cs @@ -0,0 +1,9 @@ +namespace Domain.Statuses; + +public enum ImportRequestStatus +{ + Pending, + Approved, + Rejected, + CheckedIn, +} \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 3d4ef3bb..710b935b 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -1,10 +1,13 @@ +using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using Application.Common.Interfaces; +using Application.Common.Models; using Infrastructure.Identity; using Infrastructure.Identity.Authentication; using Infrastructure.Persistence; using Infrastructure.Services; using Infrastructure.Shared; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,6 +23,8 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddScoped(sp => sp.GetService()!); services.AddScoped(sp => sp.GetService()!); services.AddScoped(); + services.AddScoped(); + services.AddTransient(); services.AddMailService(configuration); services.AddJweAuthentication(configuration); @@ -82,7 +87,7 @@ private static IServiceCollection AddJweAuthentication(this IServiceCollection s services.AddSingleton(encryptionKey); services.AddSingleton(signingKey); services.AddSingleton(tokenValidationParameters); - + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(JweAuthenticationOptions.DefaultScheme) .AddScheme(JweAuthenticationOptions.DefaultScheme, options => diff --git a/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs b/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs index 795d1585..8c706291 100644 --- a/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs +++ b/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs @@ -40,6 +40,10 @@ protected override async Task HandleAuthenticateAsync() var claimsPrincipal = handler.ValidateToken(token, Options.TokenValidationParameters, out var validatedToken); + if (claimsPrincipal.Claims.Single(x => x.Type.Equals("isActive")).Value.Equals(false.ToString())) + { + return AuthenticateResult.Fail("User is not active."); + } Context.User = claimsPrincipal; return validatedToken is null diff --git a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs index 863a36d7..469e1e98 100644 --- a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs +++ b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs @@ -1,3 +1,4 @@ +using System.IdentityModel.Tokens.Jwt; using Application.Identity; using Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; @@ -20,8 +21,7 @@ public void OnAuthorization(AuthorizationFilterContext context) { var dbContext = context.HttpContext.RequestServices.GetRequiredService(); - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - var email = context.HttpContext.User.Claims.SingleOrDefault(y => y.Type.Equals(emailClaim))!.Value; + var email = context.HttpContext.User.Claims.SingleOrDefault(y => y.Type.Equals(JwtRegisteredClaimNames.Email))!.Value; var user = dbContext.Users.FirstOrDefault(x => x.Email!.Equals(email)); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index ef335a22..ddf84780 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -60,9 +60,7 @@ public async Task Validate(string token, string refreshToken) return false; } - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - - var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; + var email = validatedToken.Claims.Single(y => y.Type.Equals(JwtRegisteredClaimNames.Email)).Value; var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) @@ -118,12 +116,12 @@ public async Task RefreshTokenAsync(string token, string r { throw new AuthenticationException("Invalid token."); } - - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; + var email = validatedToken.Claims.Single(y => y.Type.Equals(JwtRegisteredClaimNames.Email)).Value; - var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -197,7 +195,7 @@ public async Task RefreshTokenAsync(string token, string r { var user = _applicationDbContext.Users .Include(x => x.Department) - .FirstOrDefault(x => x.Email!.Equals(email)); + .FirstOrDefault(x => x.Email.ToLower().Equals(email.Trim().ToLower())); if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) { @@ -260,7 +258,7 @@ public async Task ResetPassword(string token, string newPassword) } var salt = StringUtil.RandomSalt(); user.PasswordSalt = salt; - user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); + user.PasswordHash = newPassword.HashPasswordWith(salt, _securitySettings.Pepper); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); @@ -293,10 +291,13 @@ private SecurityToken CreateJweToken(User user) var utcNow = DateTime.UtcNow; var authClaims = new List { + new(JwtRegisteredClaimNames.NameId, user.Id.ToString()), new(JwtRegisteredClaimNames.Sub, user.Username), new(JwtRegisteredClaimNames.Email, user.Email!), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(JwtRegisteredClaimNames.Iat, utcNow.ToString(CultureInfo.InvariantCulture)), + new("departmentId", user.Department is not null ? user.Department.Id.ToString() : Guid.Empty.ToString()), + new("isActive", user.IsActive.ToString()), }; var publicEncryptionKey = new RsaSecurityKey(_encryptionKey.ExportParameters(false)) {KeyId = _jweSettings.EncryptionKeyId}; var privateSigningKey = new ECDsaSecurityKey(_signingKey) {KeyId = _jweSettings.SigningKeyId}; diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 8b76879b..82d1f679 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 91c14162..c842828d 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -2,6 +2,7 @@ using Application.Common.Interfaces; using Domain.Entities; using Domain.Entities.Digital; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Infrastructure.Common; using MediatR; @@ -26,7 +27,9 @@ public ApplicationDbContext( public DbSet Lockers => Set(); public DbSet Folders => Set(); public DbSet Documents => Set(); + public DbSet ImportRequests => Set(); public DbSet Borrows => Set(); + public DbSet Permissions => Set(); public DbSet UserGroups => Set(); public DbSet Files => Set(); @@ -34,6 +37,14 @@ public ApplicationDbContext( public DbSet RefreshTokens => Set(); public DbSet ResetPasswordTokens => Set(); + + public DbSet RoomLogs => Set(); + public DbSet LockerLogs => Set(); + public DbSet FolderLogs => Set(); + public DbSet DocumentLogs => Set(); + public DbSet RequestLogs => Set(); + public DbSet UserLogs => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index f788d804..394444b2 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -86,11 +86,6 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = department; await context.Users.AddAsync(admin); } - if (context.Users.All(u => u.Username != staff.Username)) - { - staff.Department = department; - await context.Users.AddAsync(staff); - } } else { @@ -100,11 +95,6 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = departmentEntity; await context.Users.AddAsync(admin); } - if (context.Users.All(u => u.Username != staff.Username)) - { - staff.Department = departmentEntity; - await context.Users.AddAsync(staff); - } } if (context.Departments.All(u => u.Name != itDepartment.Name)) @@ -115,6 +105,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = itDepartment; await context.Users.AddAsync(employee); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = department; + await context.Users.AddAsync(staff); + } } else { @@ -124,6 +119,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = departmentEntity; await context.Users.AddAsync(employee); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = departmentEntity; + await context.Users.AddAsync(staff); + } } await context.SaveChangesAsync(); diff --git a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs index 78697f26..7b855437 100644 --- a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs @@ -28,7 +28,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.DueTime) .IsRequired(); - builder.Property(x => x.Reason) + builder.Property(x => x.BorrowReason) .IsRequired(); builder.Property(x => x.Status) diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index a9d89058..3bb00c66 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -36,7 +36,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(x => x.Importer) .WithMany() - .HasForeignKey("ImporterId") + .HasForeignKey(x => x.ImporterId) .IsRequired(false); builder.Property(x => x.Status) @@ -46,5 +46,8 @@ public void Configure(EntityTypeBuilder builder) .WithOne() .HasForeignKey(x => x.EntryId) .IsRequired(false); + + builder.Property(x => x.IsPrivate) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs new file mode 100644 index 00000000..c9e4e1b6 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs @@ -0,0 +1,43 @@ +using Domain.Entities.Physical; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class ImportRequestConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasOne(x => x.Document) + .WithOne() + .HasForeignKey(x => x.DocumentId) + .IsRequired(); + + builder.HasOne(x => x.Room) + .WithMany() + .HasForeignKey(x => x.RoomId) + .IsRequired(); + + builder.Property(x => x.ImportReason) + .IsRequired(); + + builder.Property(x => x.Status) + .IsRequired(); + + builder.Property(x => x.Created) + .IsRequired(); + + builder.Property(x => x.CreatedBy) + .IsRequired(false); + + builder.Property(x => x.LastModified) + .IsRequired(false); + + builder.Property(x => x.LastModifiedBy) + .IsRequired(false); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs b/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs new file mode 100644 index 00000000..0f5db055 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs @@ -0,0 +1,16 @@ +using Domain.Entities.Physical; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class PermissionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => new { x.DocumentId, x.EmployeeId }); + + builder.Property(x => x.AllowedOperations) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs index 2ad65961..98b4fea3 100644 --- a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs @@ -1,4 +1,3 @@ -using Domain.Entities; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -27,10 +26,16 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.NumberOfLockers) .IsRequired(); + builder.Property(x => x.Capacity) .IsRequired(); builder.Property(x => x.IsAvailable) .IsRequired(); + + builder.HasOne(x => x.Department) + .WithMany(x => x.Rooms) + .HasForeignKey(x => x.DepartmentId) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs new file mode 100644 index 00000000..09f6db4c --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs @@ -0,0 +1,20 @@ +using Domain.Entities.Logging; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class UserLogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasOne(x => x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs new file mode 100644 index 00000000..69cd1b45 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs @@ -0,0 +1,879 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612222216_Logging")] + partial class Logging + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs new file mode 100644 index 00000000..322b6a3c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs @@ -0,0 +1,381 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class Logging : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "Rooms", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Rooms", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Rooms", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Rooms", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Lockers", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Lockers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Lockers", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Lockers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Folders", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Folders", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Folders", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Folders", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Documents", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Documents", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Borrows", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Borrows", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Borrows", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Borrows", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "DocumentLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DocumentLogs", x => x.Id); + table.ForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + column: x => x.ObjectId, + principalTable: "Documents", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_DocumentLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FolderLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FolderLogs", x => x.Id); + table.ForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + column: x => x.ObjectId, + principalTable: "Folders", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_FolderLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LockerLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LockerLogs", x => x.Id); + table.ForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + column: x => x.ObjectId, + principalTable: "Lockers", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_LockerLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RoomLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomLogs", x => x.Id); + table.ForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + column: x => x.ObjectId, + principalTable: "Rooms", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RoomLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_UserId", + table: "DocumentLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_UserId", + table: "FolderLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_UserId", + table: "LockerLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_UserId", + table: "RoomLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DocumentLogs"); + + migrationBuilder.DropTable( + name: "FolderLogs"); + + migrationBuilder.DropTable( + name: "LockerLogs"); + + migrationBuilder.DropTable( + name: "RoomLogs"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Borrows"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs new file mode 100644 index 00000000..303b1ca8 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs @@ -0,0 +1,917 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612223900_Permission")] + partial class Permission + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs new file mode 100644 index 00000000..d910fecb --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class Permission : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + EmployeeId = table.Column(type: "uuid", nullable: false), + DocumentId = table.Column(type: "uuid", nullable: false), + AllowedOperations = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => new { x.DocumentId, x.EmployeeId }); + table.ForeignKey( + name: "FK_Permissions_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Permissions_Users_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_EmployeeId", + table: "Permissions", + column: "EmployeeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Permissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs new file mode 100644 index 00000000..b34b7bfe --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs @@ -0,0 +1,965 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612230312_RequestLog")] + partial class RequestLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs new file mode 100644 index 00000000..e06dcb91 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RequestLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RequestLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RequestLogs", x => x.Id); + table.ForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + column: x => x.ObjectId, + principalTable: "Documents", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RequestLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_UserId", + table: "RequestLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RequestLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs new file mode 100644 index 00000000..0974cf5a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs @@ -0,0 +1,968 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612231014_DocumentVisibility")] + partial class DocumentVisibility + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs new file mode 100644 index 00000000..673b2ad5 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DocumentVisibility : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPrivate", + table: "Documents", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPrivate", + table: "Documents"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs new file mode 100644 index 00000000..ea6e1761 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs @@ -0,0 +1,1015 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612231759_UserLog")] + partial class UserLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs new file mode 100644 index 00000000..c26f9e4b --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UserLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: false), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogs", x => x.Id); + table.ForeignKey( + name: "FK_UserLogs_Users_ObjectId", + column: x => x.ObjectId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_UserId", + table: "UserLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs new file mode 100644 index 00000000..710cd220 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs @@ -0,0 +1,1019 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230613140433_RequestLogReason")] + partial class RequestLogReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs new file mode 100644 index 00000000..5ca28aa1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RequestLogReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reason", + table: "RequestLogs", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reason", + table: "RequestLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs new file mode 100644 index 00000000..9b76af20 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs @@ -0,0 +1,1022 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230614084428_PermissionExpiry")] + partial class PermissionExpiry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs new file mode 100644 index 00000000..61e9daaa --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class PermissionExpiry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExpiryDateTime", + table: "Permissions", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExpiryDateTime", + table: "Permissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs new file mode 100644 index 00000000..aa6b5101 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs @@ -0,0 +1,1021 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230616101721_DepartmentHasManyRooms")] + partial class DepartmentHasManyRooms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs new file mode 100644 index 00000000..4d7f402c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DepartmentHasManyRooms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId", + unique: true); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs new file mode 100644 index 00000000..dc0a573d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs @@ -0,0 +1,1076 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618044742_AddImportRequest")] + partial class AddImportRequest + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs new file mode 100644 index 00000000..67c80471 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddImportRequest : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImportRequests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RoomId = table.Column(type: "uuid", nullable: false), + DocumentId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: true), + LastModified = table.Column(type: "timestamp without time zone", nullable: true), + LastModifiedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportRequests", x => x.Id); + table.ForeignKey( + name: "FK_ImportRequests_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ImportRequests_Rooms_RoomId", + column: x => x.RoomId, + principalTable: "Rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_RoomId", + table: "ImportRequests", + column: "RoomId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ImportRequests"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs new file mode 100644 index 00000000..a64e8b12 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs @@ -0,0 +1,1080 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618051340_AddImportRequestReason")] + partial class AddImportRequestReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs new file mode 100644 index 00000000..930e4718 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddImportRequestReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reason", + table: "ImportRequests", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reason", + table: "ImportRequests"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs new file mode 100644 index 00000000..531983a1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs @@ -0,0 +1,1110 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618133410_LoggingNowHasBaseObject")] + partial class LoggingNowHasBaseObject + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs new file mode 100644 index 00000000..64028226 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs @@ -0,0 +1,139 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class LoggingNowHasBaseObject : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests"); + + migrationBuilder.DropColumn( + name: "Reason", + table: "RequestLogs"); + + migrationBuilder.AddColumn( + name: "BaseRoomId", + table: "LockerLogs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "BaseLockerId", + table: "FolderLogs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "BaseFolderId", + table: "DocumentLogs", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_BaseRoomId", + table: "LockerLogs", + column: "BaseRoomId"); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_BaseLockerId", + table: "FolderLogs", + column: "BaseLockerId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_BaseFolderId", + table: "DocumentLogs", + column: "BaseFolderId"); + + migrationBuilder.AddForeignKey( + name: "FK_DocumentLogs_Folders_BaseFolderId", + table: "DocumentLogs", + column: "BaseFolderId", + principalTable: "Folders", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderLogs_Lockers_BaseLockerId", + table: "FolderLogs", + column: "BaseLockerId", + principalTable: "Lockers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_LockerLogs_Rooms_BaseRoomId", + table: "LockerLogs", + column: "BaseRoomId", + principalTable: "Rooms", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DocumentLogs_Folders_BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_FolderLogs_Lockers_BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_LockerLogs_Rooms_BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_LockerLogs_BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests"); + + migrationBuilder.DropIndex( + name: "IX_FolderLogs_BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropIndex( + name: "IX_DocumentLogs_BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.DropColumn( + name: "BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropColumn( + name: "BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropColumn( + name: "BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.AddColumn( + name: "Reason", + table: "RequestLogs", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs new file mode 100644 index 00000000..77f26586 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs @@ -0,0 +1,1060 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230620131154_LoggingNowHasObjectId")] + partial class LoggingNowHasObjectId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs new file mode 100644 index 00000000..28d5c38d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class LoggingNowHasObjectId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + table: "DocumentLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + table: "FolderLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + table: "LockerLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + table: "RequestLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + table: "RoomLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_UserLogs_Users_ObjectId", + table: "UserLogs"); + + migrationBuilder.DropIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs"); + + migrationBuilder.DropIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs"); + + migrationBuilder.DropIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs"); + + migrationBuilder.DropIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs"); + + migrationBuilder.DropIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs"); + + migrationBuilder.AlterColumn( + name: "ObjectId", + table: "UserLogs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ObjectId", + table: "UserLogs", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs", + column: "ObjectId"); + + migrationBuilder.AddForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + table: "DocumentLogs", + column: "ObjectId", + principalTable: "Documents", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + table: "FolderLogs", + column: "ObjectId", + principalTable: "Folders", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + table: "LockerLogs", + column: "ObjectId", + principalTable: "Lockers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + table: "RequestLogs", + column: "ObjectId", + principalTable: "Documents", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + table: "RoomLogs", + column: "ObjectId", + principalTable: "Rooms", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_UserLogs_Users_ObjectId", + table: "UserLogs", + column: "ObjectId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs new file mode 100644 index 00000000..938aeb1a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs @@ -0,0 +1,1068 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230621135501_AddMoreReason")] + partial class AddMoreReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs new file mode 100644 index 00000000..04890a1d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddMoreReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Reason", + table: "ImportRequests", + newName: "StaffReason"); + + migrationBuilder.RenameColumn( + name: "Reason", + table: "Borrows", + newName: "StaffReason"); + + migrationBuilder.AddColumn( + name: "ImportReason", + table: "ImportRequests", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "BorrowReason", + table: "Borrows", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImportReason", + table: "ImportRequests"); + + migrationBuilder.DropColumn( + name: "BorrowReason", + table: "Borrows"); + + migrationBuilder.RenameColumn( + name: "StaffReason", + table: "ImportRequests", + newName: "Reason"); + + migrationBuilder.RenameColumn( + name: "StaffReason", + table: "Borrows", + newName: "Reason"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 9fab7c8c..ba5d9a63 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -104,6 +104,180 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UserGroups"); }); + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -113,19 +287,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ActualReturnTime") .HasColumnType("timestamp without time zone"); + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + b.Property("BorrowTime") .HasColumnType("timestamp without time zone"); b.Property("BorrowerId") .HasColumnType("uuid"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DocumentId") .HasColumnType("uuid"); b.Property("DueTime") .HasColumnType("timestamp without time zone"); - b.Property("Reason") + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") .IsRequired() .HasColumnType("text"); @@ -147,6 +337,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DepartmentId") .HasColumnType("uuid"); @@ -168,6 +364,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Status") .HasColumnType("integer"); @@ -199,6 +404,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -206,6 +417,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("LockerId") .HasColumnType("uuid"); @@ -224,6 +441,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Folders"); }); + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => { b.Property("Id") @@ -233,6 +495,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -240,6 +508,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -258,6 +532,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Lockers"); }); + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Room", b => { b.Property("Id") @@ -267,6 +563,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DepartmentId") .HasColumnType("uuid"); @@ -277,6 +579,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -289,8 +597,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasAlternateKey("Name"); - b.HasIndex("DepartmentId") - .IsUnique(); + b.HasIndex("DepartmentId"); b.ToTable("Rooms"); }); @@ -467,6 +774,90 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("File"); }); + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") @@ -524,6 +915,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Locker"); }); + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => { b.HasOne("Domain.Entities.Physical.Room", "Room") @@ -535,11 +945,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Room"); }); + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Room", b => { b.HasOne("Domain.Entities.Department", "Department") - .WithOne("Room") - .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -611,7 +1040,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Department", b => { - b.Navigation("Room"); + b.Navigation("Rooms"); }); modelBuilder.Entity("Domain.Entities.Physical.Folder", b => diff --git a/src/Infrastructure/Services/DateTimeService.cs b/src/Infrastructure/Services/DateTimeService.cs new file mode 100644 index 00000000..bfed60e1 --- /dev/null +++ b/src/Infrastructure/Services/DateTimeService.cs @@ -0,0 +1,8 @@ +using Application.Common.Interfaces; + +namespace Infrastructure.Services; + +public class DateTimeService : IDateTimeProvider +{ + public DateTime DateTimeNow => DateTime.Now; +} \ No newline at end of file diff --git a/src/Infrastructure/Services/PermissionManager.cs b/src/Infrastructure/Services/PermissionManager.cs new file mode 100644 index 00000000..81137b75 --- /dev/null +++ b/src/Infrastructure/Services/PermissionManager.cs @@ -0,0 +1,94 @@ +using System.Configuration; +using Application.Common.Interfaces; +using Application.Common.Models.Operations; +using Domain.Entities; +using Domain.Entities.Physical; +using NodaTime; + +namespace Infrastructure.Services; + +public class PermissionManager : IPermissionManager +{ + private readonly IApplicationDbContext _context; + + public PermissionManager(IApplicationDbContext context) + { + _context = context; + } + + public bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds) + { + return Array.TrueForAll(userIds, id => _context.Permissions.Any(x => + x.DocumentId == documentId + && x.EmployeeId == id + && x.AllowedOperations.Contains(operation.ToString()))); + } + + public async Task GrantAsync(Document document, DocumentOperation operation, User[] users, DateTime expiryDate, CancellationToken cancellationToken) + { + foreach (var user in users) + { + var existedPermission = + _context.Permissions.FirstOrDefault(x => x.DocumentId == document.Id && x.EmployeeId == user.Id); + + if (existedPermission is not null) + { + var operations = existedPermission.AllowedOperations.Split(","); + if (operations.Contains(operation.ToString())) + { + continue; + } + + var x = new CommaDelimitedStringCollection + { + existedPermission.AllowedOperations, + operation.ToString() + }; + existedPermission.AllowedOperations = x.ToString(); + existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); + _context.Permissions.Update(existedPermission); + } + else + { + existedPermission = new Permission() + { + DocumentId = document.Id, + EmployeeId = user.Id, + Document = document, + Employee = user, + AllowedOperations = operation.ToString(), + }; + existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); + await _context.Permissions.AddAsync(existedPermission, cancellationToken); + } + } + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task RevokeAsync(Guid documentId, DocumentOperation operation, Guid[] userIds, CancellationToken cancellationToken) + { + foreach (var userId in userIds) + { + var existedPermission = + _context.Permissions.FirstOrDefault(x => x.DocumentId == documentId && x.EmployeeId == userId); + if (existedPermission is null) continue; + var operations = existedPermission.AllowedOperations.Split(","); + if (!operations.Contains(operation.ToString())) continue; + + var x = new CommaDelimitedStringCollection(); + x.AddRange(operations); + x.Remove(operation.ToString()); + if (x.Count == 0 || existedPermission.ExpiryDateTime < LocalDateTime.FromDateTime(DateTime.Now)) + { + _context.Permissions.Remove(existedPermission); + } + else + { + existedPermission.AllowedOperations = x.ToString(); + _context.Permissions.Update(existedPermission); + } + } + await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index eaa0afa5..f4644e5e 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -242,7 +242,7 @@ protected static Borrow CreateBorrowRequest(User borrower, Document document, Bo Id = Guid.NewGuid(), Borrower = borrower, Document = document, - Reason = "something something", + BorrowReason = "something something", Status = status, BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), DueTime = LocalDateTime.FromDateTime(DateTime.Now + TimeSpan.FromDays(1)) diff --git a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs index 87a40fd6..09f8d7bd 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs @@ -1,4 +1,4 @@ -using Application.Borrows.Commands; +using Application.Borrows.Commands; using Application.Common.Exceptions; using Application.Identity; using Domain.Entities.Physical; @@ -30,7 +30,7 @@ public async Task ShouldApproveRequest_WhenRequestIsValid() await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -51,7 +51,7 @@ public async Task ShouldApproveRequest_WhenRequestIsValid() public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() { // Arrange - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = Guid.NewGuid(), }; @@ -78,7 +78,7 @@ public async Task ShouldThrowConflictException_WhenDocumentIsLost() await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -108,7 +108,7 @@ public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPendingAndR await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -146,7 +146,7 @@ public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnAppro await context.AddAsync(request2); await context.SaveChangesAsync(); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request2.Id, }; diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs index b595d20f..ee5a8708 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -42,7 +42,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() { BorrowerId = user.Id, DocumentId = document.Id, - Reason = "Example", + BorrowReason = "Example", BorrowFrom = DateTime.Now.Add(TimeSpan.FromHours(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(1)), }; @@ -53,7 +53,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() // Assert result.DocumentId.Should().Be(command.DocumentId); result.BorrowerId.Should().Be(command.BorrowerId); - result.Reason.Should().Be(command.Reason); + result.BorrowReason.Should().Be(command.BorrowReason); result.BorrowTime.Should().Be(command.BorrowFrom); result.DueTime.Should().Be(command.BorrowTo); result.Status.Should().Be(BorrowRequestStatus.Pending.ToString()); @@ -79,7 +79,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -111,7 +111,7 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActive() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -144,7 +144,7 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActivated() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -172,7 +172,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() DocumentId = Guid.NewGuid(), BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -203,7 +203,7 @@ public async Task ShouldConflictException_WhenDocumentIsNotAvailable() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -244,7 +244,7 @@ public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheS DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -287,7 +287,7 @@ public async Task ShouldThrowConflictException_WhenRequestWithSameUserAndDocumen DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -334,7 +334,7 @@ public async Task ShouldThrowConflictException_WhenARequestIsMadeWhileDocumentIs DocumentId = document.Id, BorrowFrom = DateTime.Now.AddHours(1), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act diff --git a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs index f00d3e25..841a1f04 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs @@ -32,7 +32,7 @@ public async Task ShouldUpdateBorrow_WhenDetailsAreValid() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -42,7 +42,7 @@ public async Task ShouldUpdateBorrow_WhenDetailsAreValid() var result = await SendAsync(command); // Assert - result.Reason.Should().Be(command.Reason); + result.BorrowReason.Should().Be(command.BorrowReason); result.BorrowTime.Should().Be(command.BorrowFrom); result.DueTime.Should().Be(command.BorrowTo); @@ -58,7 +58,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() // Arrange var command = new UpdateBorrow.Command() { - Reason = "adsda", + BorrowReason = "adsda", BorrowFrom = DateTime.Now.AddHours(1), BorrowTo = DateTime.Now.AddHours(2), BorrowId = Guid.NewGuid(), @@ -86,7 +86,7 @@ public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPending() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -120,7 +120,7 @@ public async Task ShouldThrowConflictException_WhenDocumentIsLost() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -163,7 +163,7 @@ public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnAppro var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(5), BorrowTo = DateTime.Now.AddDays(13), BorrowId = borrow1.Id, diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs deleted file mode 100644 index cf2628d7..00000000 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Application.Common.Exceptions; -using Application.Folders.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Folders.Commands; - -public class DisableFolderTests : BaseClassFixture -{ - public DisableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var disabledFolder = await SendAsync(disableFolderCommand); - - // Assert - disabledFolder.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenFolderDoesNotExist() - { - // Arrange - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = Guid.NewGuid() - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder does not exist."); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - folder.IsAvailable = false; - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder has already been disabled."); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenFolderHasDocuments() - { - // Arrange - var department = CreateDepartment(); - var document = CreateNDocuments(1).First(); - var folder = CreateFolder(document); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder cannot be disabled because it contains documents."); - - // Cleanup - Remove(document); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs deleted file mode 100644 index 62f0d165..00000000 --- a/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Application.Common.Exceptions; -using Application.Folders.Commands; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Folders.Commands; - -public class EnableFolderTests : BaseClassFixture -{ - public EnableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldEnableFolder_WhenThatFolderExistsAndIsDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - folder.IsAvailable = false; - await AddAsync(room); - - var command = new EnableFolder.Command() - { - FolderId = folder.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.IsAvailable.Should().BeTrue(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() - { - // Arrange - var command = new EnableFolder.Command() - { - FolderId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenFolderIsAlreadyAvailable() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - folder.IsAvailable = true; - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new EnableFolder.Command() - { - FolderId = folder.Id, - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder has already been enabled."); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs deleted file mode 100644 index 5cf76d23..00000000 --- a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Application.Common.Exceptions; -using Application.Lockers.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Commands; - -public class DisableLockerTests : BaseClassFixture -{ - public DisableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableLocker_WhenLockerExistsAndIsAvailable() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - var result = await SendAsync(disableLockerCommand); - - // Assert - result.Name.Should().Be(locker.Name); - result.Description.Should().Be(locker.Description); - result.Capacity.Should().Be(locker.Capacity); - result.IsAvailable.Should().BeFalse(); - result.NumberOfFolders.Should().Be(locker.NumberOfFolders); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() - { - // Arrange - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(disableLockerCommand); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenLockerIsAlreadyDisabled() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - await SendAsync(disableLockerCommand); - var action = async () => await SendAsync(disableLockerCommand); - - // Assert - await action.Should().ThrowAsync().WithMessage("Locker has already been disabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs deleted file mode 100644 index b4ec3df7..00000000 --- a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Application.Common.Exceptions; -using Application.Lockers.Commands; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Commands; - -public class EnableLockerTests : BaseClassFixture -{ - public EnableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) - { - - } - - [Fact] - public async Task ShouldEnableLocker_WhenLockerExistsAndIsNotAvailable() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - locker.IsAvailable = false; - var room = CreateRoom(department, locker); - await AddAsync(room); - - // Act - var command = new EnableLocker.Command() - { - LockerId = locker.Id, - }; - - var result = await SendAsync(command); - - // Assert - result.IsAvailable.Should().BeTrue(); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() - { - // Arrange - var command = new EnableLocker.Command() - { - LockerId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenLockerIsAlreadyEnabled() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var enableLockerCommand = new EnableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - var action = async () => await SendAsync(enableLockerCommand); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker has already been enabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} diff --git a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs deleted file mode 100644 index 39badcb8..00000000 --- a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Application.Common.Exceptions; -using Application.Rooms.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Rooms.Commands; - -public class DisableRoomTests : BaseClassFixture -{ - public DisableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableRoom_WhenRoomHaveNoDocument() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(disableRoomCommand); - - // Assert - var folderResult = await FindAsync(folder.Id); - var lockerResult = await FindAsync(locker.Id); - - result.IsAvailable.Should().BeFalse(); - folderResult.IsAvailable.Should().BeFalse(); - lockerResult.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() - { - // Arrange - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(disableRoomCommand); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room does not exist."); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotEmptyOfDocuments() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(1); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - - await AddAsync(room); - - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(disableRoomCommand); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room cannot be disabled because it contains documents."); - - // Cleanup - Remove(documents.First()); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotAvailable() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - room.IsAvailable = false; - await AddAsync(room); - - var command = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room have already been disabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs deleted file mode 100644 index fa07d0b8..00000000 --- a/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Application.Common.Exceptions; -using Application.Rooms.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Rooms.Commands; - -public class EnableRoomTests : BaseClassFixture -{ - public EnableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldEnableRoom_WhenRoomExistsAndIsDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - folder.IsAvailable = false; - locker.IsAvailable = false; - room.IsAvailable = false; - await AddAsync(room); - - var command = new EnableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(command); - - // Assert - var folderResult = await FindAsync(folder.Id); - var lockerResult = await FindAsync(locker.Id); - - result.IsAvailable.Should().BeTrue(); - folderResult!.IsAvailable.Should().BeFalse(); - lockerResult!.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() - { - // Arrange - var command = new EnableRoom.Command() - { - RoomId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenRoomIsAlreadyAvailable() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - room.IsAvailable = true; - await AddAsync(room); - - var command = new EnableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room has already been enabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs deleted file mode 100644 index 92df91b8..00000000 --- a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Application.Identity; -using Application.Staffs.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Staffs.Commands; - -public class RemoveStaffTests : BaseClassFixture -{ - public RemoveStaffTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldRemoveStaff_WhenStaffIdIsValid() - { - // Arrange - var department = CreateDepartment(); - var user = CreateUser(IdentityData.Roles.Admin, "123456"); - var room = CreateRoom(department); - var staff = CreateStaff(user, room); - await AddAsync(staff); - - var command = new RemoveStaff.Command() - { - StaffId = staff.Id - }; - - // Act - await SendAsync(command); - - // Assert - var result = await FindAsync(staff.Id); - result.Should().BeNull(); - - // Cleanup - Remove(await FindAsync(room.Id)); - Remove(user); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() - { - // Arrange - var command = new RemoveStaff.Command() - { - StaffId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Staff does not exist."); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs index 4c0547ef..8033323b 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs @@ -66,8 +66,7 @@ public async Task ShouldReturnASpecificStaff() // Assert result.TotalCount.Should().Be(1); result.Items.First().Should() - .BeEquivalentTo(_mapper.Map(staff1), - config => config.Excluding(x => x.User.Created)); + .BeEquivalentTo(_mapper.Map(staff1)); // Cleanup Remove(staff2); diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs index 123e2542..b0da147c 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs @@ -31,7 +31,7 @@ public async Task ShouldReturnStaff_WhenRoomHaveStaff() var staff = CreateStaff(user, room); await AddAsync(staff); - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = room.Id }; @@ -57,7 +57,7 @@ public async Task ShouldReturnStaff_WhenRoomHaveStaff() public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() { // Arrange - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = Guid.NewGuid() }; @@ -78,7 +78,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotHaveStaff() var room = CreateRoom(department); await AddAsync(room); - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = room.Id }; diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 49db0eca..7f6a4ac8 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -2,6 +2,7 @@ using Application.Common.Mappings; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; using AutoMapper; @@ -48,6 +49,9 @@ public void ShouldHaveValidConfiguration() [InlineData(typeof(FileEntity), typeof(FileDto))] [InlineData(typeof(Entry), typeof(EntryDto))] [InlineData(typeof(UserGroup), typeof(UserGroupDto))] + [InlineData(typeof(User), typeof(IssuerDto))] + [InlineData(typeof(Document), typeof(IssuedDocumentDto))] + [InlineData(typeof(ImportRequest), typeof(ImportRequestDto))] public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination) { // Arrange From 6dd51912e678c1f4135434ba0f4e6225a7486c06 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Thu, 22 Jun 2023 15:52:02 +0700 Subject: [PATCH 062/162] refactor: seperate cors rules for environments (#226) --- docker-compose.yml | 9 +++++++++ src/Api/Common/CORSPolicy.cs | 7 +++++++ src/Api/ConfigureServices.cs | 18 +++++++++++++++--- src/Api/Extensions/WebApplicationExtensions.cs | 12 +++++++++++- src/Api/Program.cs | 2 +- 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/Api/Common/CORSPolicy.cs diff --git a/docker-compose.yml b/docker-compose.yml index 4ba0a076..295d1679 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,15 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured; + - PROFILE_JweSettings__SigningKeyId=4bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b50144bd28be8eac5414fb01c5cbe343b5014 + - PROFILE_JweSettings__EncryptionKeyId=4bd28be8eac5414fb01c5cbe343b5014 + - PROFILE_JweSettings__TokenLifetime=00:20:00 + - PROFILE_JweSettings__RefreshTokenLifetimeInDays=3 + - PROFILE_MailSettings__ClientUrl=https://send.api.mailtrap.io/api/send + - PROFILE_MailSettings__Token=745f040659edff0ce87b545567da72d2 + - PROFILE_MailSettings__SenderName=ProFile + - PROFILE_MailSettings__SenderEmail=profile@ezarp.dev + - PROFILE_MailSettings__TemplateUuid=9d6a8f25-65e9-4819-be7d-106ce077acf1 depends_on: database: condition: service_started diff --git a/src/Api/Common/CORSPolicy.cs b/src/Api/Common/CORSPolicy.cs new file mode 100644 index 00000000..a65f0d1b --- /dev/null +++ b/src/Api/Common/CORSPolicy.cs @@ -0,0 +1,7 @@ +namespace Api.Common; + +public static class CORSPolicy +{ + public static string Development = "devEnvironment"; + public static string Production = "prodEnvironment"; +} \ No newline at end of file diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 0a8f60f0..c0029e4d 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Api.Common; using Api.Middlewares; using Api.Policies; using Api.Services; @@ -10,8 +11,11 @@ namespace Api; public static class ConfigureServices { - public static IServiceCollection AddApiServices(this IServiceCollection services) + public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) { + // TODO: Move this away in the future + var frontendBaseUrl = configuration.GetValue("BASE_FRONTEND_URL") ?? "http://localhost"; + // Register services services.AddServices(); services.AddHostedService(); @@ -23,9 +27,17 @@ public static IServiceCollection AddApiServices(this IServiceCollection services services.AddCors(options => { - options.AddPolicy("AllowAllOrigins", builder => + options.AddPolicy(CORSPolicy.Development, builder => + { + builder.SetIsOriginAllowed(_ => true); + builder.AllowAnyHeader(); + builder.AllowAnyMethod(); + builder.AllowCredentials(); + }); + + options.AddPolicy(CORSPolicy.Production, builder => { - builder.WithOrigins("http://localhost:3000"); + builder.WithOrigins(frontendBaseUrl); builder.AllowAnyHeader(); builder.AllowAnyMethod(); builder.AllowCredentials(); diff --git a/src/Api/Extensions/WebApplicationExtensions.cs b/src/Api/Extensions/WebApplicationExtensions.cs index 63e834ee..86506f58 100644 --- a/src/Api/Extensions/WebApplicationExtensions.cs +++ b/src/Api/Extensions/WebApplicationExtensions.cs @@ -1,3 +1,4 @@ +using Api.Common; using Api.Middlewares; using Infrastructure.Persistence; using Serilog; @@ -18,7 +19,7 @@ public static void UseInfrastructure(this WebApplication app, IConfiguration con app.UseSwagger(); app.UseSwaggerUI(); - app.UseCors("AllowAllOrigins"); + app.UseCors(CORSPolicy.Development); app.MigrateDatabase((context, _) => { @@ -33,6 +34,15 @@ public static void UseInfrastructure(this WebApplication app, IConfiguration con }); } + if (app.Environment.IsEnvironment("Production")) + { + app.UseCors(CORSPolicy.Production); + app.MigrateDatabase((_, _) => + { + // TODO: should only generate admin account + }); + } + app.UseAuthentication(); app.UseAuthorization(); diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 2a739649..9c5fb823 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -16,7 +16,7 @@ builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); - builder.Services.AddApiServices(); + builder.Services.AddApiServices(builder.Configuration); var app = builder.Build(); app.UseInfrastructure(builder.Configuration); From 87c42a8844f2005ff6c53d1fe4f97d2e0bce9493 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Thu, 22 Jun 2023 16:02:44 +0700 Subject: [PATCH 063/162] fix: src/Application/Application.csproj to reduce vulnerabilities (#260) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-DOTNET-SYSTEMSECURITYCRYPTOGRAPHYPKCS-5708426 Co-authored-by: snyk-bot --- src/Application/Application.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 2f6bfb3b..2c3d29a3 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -11,7 +11,7 @@ - + From 8072560b9c83e9dd533b8b1fb0d540588f534f7c Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:23:30 +0700 Subject: [PATCH 064/162] feat: implement add user group. (#220) * feat: implement add user group. test: added integration tests for adding user group. * Update CreateUserGroup.cs * Update CreateUserGroup.cs --- .../UserGroups/Commands/CreateUserGroup.cs | 61 +++++++++++++++++++ .../Commands/CreateUserGroupTests.cs | 58 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/Application/UserGroups/Commands/CreateUserGroup.cs create mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs diff --git a/src/Application/UserGroups/Commands/CreateUserGroup.cs b/src/Application/UserGroups/Commands/CreateUserGroup.cs new file mode 100644 index 00000000..467cc6c4 --- /dev/null +++ b/src/Application/UserGroups/Commands/CreateUserGroup.cs @@ -0,0 +1,61 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities.Digital; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.UserGroups.Commands; + +public class CreateUserGroup +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .MaximumLength(32).WithMessage("Name cannot exceed 32 characters."); + } + } + + public record Command : IRequest + { + public string Name { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var userGroup = await _context.UserGroups.FirstOrDefaultAsync(x => x.Name.ToLower().Trim().Equals(request.Name.ToLower().Trim()), cancellationToken); + + if (userGroup is not null) + { + throw new ConflictException("User group name already exists."); + } + + var entity = new UserGroup() + { + Name = request.Name.Trim(), + }; + + var result = await _context.UserGroups.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs new file mode 100644 index 00000000..ac75ecd0 --- /dev/null +++ b/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs @@ -0,0 +1,58 @@ +using Application.Common.Exceptions; +using Application.UserGroups.Commands; +using Bogus; +using Domain.Entities; +using Domain.Entities.Digital; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.UserGroups.Commands; + +public class CreateUserGroupTests : BaseClassFixture +{ + public CreateUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldCreateUserGroup_WhenCreateDetailsAreValid() + { + // Arrange + var command = new CreateUserGroup.Command() + { + Name = new Faker().Commerce.Department(), + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Name.Should().Be(command.Name); + + // Cleanup + Remove(await FindAsync(result.Id)); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenUserGroupNameExists() + { + // Arrange + var userGroup = CreateUserGroup(new User[] {}); + await AddAsync(userGroup); + + var command = new CreateUserGroup.Command() + { + Name = userGroup.Name, + }; + + // Act + var action = async () => await SendAsync(command); + + // Assert + await action.Should().ThrowAsync().WithMessage("User group name already exists."); + + // Cleanup + Remove(userGroup); + } +} \ No newline at end of file From 0a48269c480f938f9742eae0977c0d6d930d80ab Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:27:08 +0700 Subject: [PATCH 065/162] feat: implement update user group (#223) * feat: implement update user group test: add integration tests for update user group * Update UpdateUserGroup.cs --- .../UserGroups/Commands/UpdateUserGroup.cs | 63 ++++++++++++++ .../Commands/UpdateUserGroupTests.cs | 87 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/Application/UserGroups/Commands/UpdateUserGroup.cs create mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs diff --git a/src/Application/UserGroups/Commands/UpdateUserGroup.cs b/src/Application/UserGroups/Commands/UpdateUserGroup.cs new file mode 100644 index 00000000..fb6449e7 --- /dev/null +++ b/src/Application/UserGroups/Commands/UpdateUserGroup.cs @@ -0,0 +1,63 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.UserGroups.Commands; + +public class UpdateUserGroup +{ + public record Command : IRequest + { + public Guid UserGroupId { get; init; } + public string Name { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var userGroup = await _context.UserGroups + .FirstOrDefaultAsync(x => x.Id == request.UserGroupId, cancellationToken); + + if (userGroup is null) + { + throw new KeyNotFoundException("User group does not exist."); + } + + var existedUserGroup = await _context.UserGroups + .FirstOrDefaultAsync(x => x.Name.ToLower().Trim().Equals(request.Name.ToLower().Trim()) + && x.Id != userGroup.Id, cancellationToken); + + if (existedUserGroup is not null) + { + throw new ConflictException("New user group name already exists."); + } + + var updatedUserGroup = new UserGroup() + { + Id = userGroup.Id, + Name = request.Name.Trim(), + Users = userGroup.Users, + }; + + _context.UserGroups.Entry(userGroup).State = EntityState.Detached; + _context.UserGroups.Entry(updatedUserGroup).State = EntityState.Modified; + + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(updatedUserGroup); + } + } +} diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs new file mode 100644 index 00000000..c7177006 --- /dev/null +++ b/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs @@ -0,0 +1,87 @@ +using Application.Common.Exceptions; +using Application.UserGroups.Commands; +using Domain.Entities; +using Domain.Entities.Digital; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Application.Tests.Integration.UserGroups.Commands; + +public class UpdateUserGroupTests : BaseClassFixture +{ + public UpdateUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldUpdateLocker_WhenDetailsAreValid() + { + // Arrange + var userGroup = CreateUserGroup(Array.Empty()); + await AddAsync(userGroup); + + var command = new UpdateUserGroup.Command() + { + UserGroupId = userGroup.Id, + Name = "Updated name", + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Name.Should().Be(command.Name); + + // Cleanup + Remove(await FindAsync(userGroup.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() + { + // Arrange + var command = new UpdateUserGroup.Command() + { + UserGroupId = Guid.NewGuid(), + Name = "Name", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User group does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenNewUserGroupNameAlreadyExists() + { + // Arrange + var userGroup = CreateUserGroup(Array.Empty()); + await AddAsync(userGroup); + + var updateUserGroup = CreateUserGroup(Array.Empty()); + await AddAsync(updateUserGroup); + + var command = new UpdateUserGroup.Command() + { + UserGroupId = updateUserGroup.Id, + Name = userGroup.Name, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("New user group name already exists."); + + // Cleanup + Remove(userGroup); + Remove(updateUserGroup); + } +} \ No newline at end of file From 6f980e1c8e5ce9dc9e28e527cf4cd6e7f4d8e7dd Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:34:10 +0700 Subject: [PATCH 066/162] feat: implement delete user group (#222) * feat: implement delete user group test: add integration tests for deleting user group * Update DeleteUserGroup.cs * Update DeleteUserGroupTests.cs --- .../UserGroups/Commands/DeleteUserGroup.cs | 41 +++++++++++++++ .../Commands/DeleteUserGroupTests.cs | 51 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/Application/UserGroups/Commands/DeleteUserGroup.cs create mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs diff --git a/src/Application/UserGroups/Commands/DeleteUserGroup.cs b/src/Application/UserGroups/Commands/DeleteUserGroup.cs new file mode 100644 index 00000000..7e601dd8 --- /dev/null +++ b/src/Application/UserGroups/Commands/DeleteUserGroup.cs @@ -0,0 +1,41 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.UserGroups.Commands; + +public class DeleteUserGroup +{ + public record Command : IRequest + { + public Guid UserGroupId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var userGroup = await _context.UserGroups.FirstOrDefaultAsync(x => x.Id == request.UserGroupId, cancellationToken); + + if (userGroup is null) + { + throw new KeyNotFoundException("User group does not exist."); + } + + var result = _context.UserGroups.Remove(userGroup); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs new file mode 100644 index 00000000..12db7afc --- /dev/null +++ b/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs @@ -0,0 +1,51 @@ +using Application.UserGroups.Commands; +using Domain.Entities; +using Domain.Entities.Digital; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.UserGroups.Commands; + +public class DeleteUserGroupTests : BaseClassFixture +{ + public DeleteUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldDeleteUserGroup_WhenUserGroupExists() + { + // Arrange + var userGroup = CreateUserGroup(Array.Empty()); + await AddAsync(userGroup); + + var command = new DeleteUserGroup.Command() + { + UserGroupId = userGroup.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + var entityCheck = await FindAsync(result.Id); + entityCheck.Should().BeNull(); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() + { + // Arrange + var command = new DeleteUserGroup.Command() + { + UserGroupId = Guid.NewGuid(), + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync().WithMessage("User group does not exist."); + } +} From e7031a613ac882ad93de1b66f59b25be633dcfb5 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:38:23 +0700 Subject: [PATCH 067/162] feat: implement get user group (#225) * feat: implement get user groups test: added integration tests for getting user groups * naming * refactor: made get all to be paginated and added equivalent tests --- .../Queries/GetAllUserGroupsPaginated.cs | 62 ++++++ .../UserGroups/Queries/GetUserGroupById.cs | 42 +++++ .../Queries/GetAllUserGroupsPaginatedTests.cs | 176 ++++++++++++++++++ .../Queries/GetUserGroupByIdTests.cs | 53 ++++++ 4 files changed, 333 insertions(+) create mode 100644 src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs create mode 100644 src/Application/UserGroups/Queries/GetUserGroupById.cs create mode 100644 tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs create mode 100644 tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs diff --git a/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs b/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs new file mode 100644 index 00000000..d06988c2 --- /dev/null +++ b/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs @@ -0,0 +1,62 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.UserGroups.Queries; + +public class GetAllUserGroupsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + }; + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var userGroups = _context.UserGroups.AsQueryable(); + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + userGroups = userGroups.Where(x => + x.Name.ToLower().Trim().Contains(request.SearchTerm.ToLower().Trim())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(UserGroupDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await userGroups.CountAsync(cancellationToken); + var list = await userGroups + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/UserGroups/Queries/GetUserGroupById.cs b/src/Application/UserGroups/Queries/GetUserGroupById.cs new file mode 100644 index 00000000..412cf807 --- /dev/null +++ b/src/Application/UserGroups/Queries/GetUserGroupById.cs @@ -0,0 +1,42 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.UserGroups.Queries; + +public class GetUserGroupById +{ + public record Query : IRequest + { + public Guid UserGroupId { get; set; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var userGroup = await _context.UserGroups + .FirstOrDefaultAsync( + x => x.Id == request.UserGroupId + , cancellationToken); + + if (userGroup is null) + { + throw new KeyNotFoundException("User group does not exist."); + } + + return _mapper.Map(userGroup); + } + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs b/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs new file mode 100644 index 00000000..d9608e01 --- /dev/null +++ b/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs @@ -0,0 +1,176 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Digital; +using Application.UserGroups.Queries; +using AutoMapper; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.UserGroups.Queries; + +public class GetAllUserGroupsPaginatedTests : BaseClassFixture +{ + private readonly IMapper _mapper; + public GetAllUserGroupsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) + { + var configuration = new MapperConfiguration(x => x.AddProfile()); + _mapper = configuration.CreateMapper(); + } + + [Fact] + public async Task ShouldReturnUserGroups_WhenUserGroupsExist() + { + // Arrange + var userGroup1 = CreateUserGroup(Array.Empty()); + var userGroup2 = CreateUserGroup(Array.Empty()); + + await AddAsync(userGroup1); + await AddAsync(userGroup2); + var query = new GetAllUserGroupsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new [] {userGroup1, userGroup2})); + + // Cleanup + Remove(userGroup1); + Remove(userGroup2); + } + + [Fact] + public async Task ShouldReturnEmptyList_WhenNoUserGroupsExist() + { + // Arrange + var query = new GetAllUserGroupsPaginated.Query(); + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(0); + } + + [Fact] + public async Task ShouldReturnAscendingOrderById_WhenWrongSortByIsProvided() + { + // Arrange + var userGroup1 = CreateUserGroup(Array.Empty()); + var userGroup2 = CreateUserGroup(Array.Empty()); + + await AddAsync(userGroup1); + await AddAsync(userGroup2); + var query = new GetAllUserGroupsPaginated.Query() + { + SortBy = "example" + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) + .OrderBy(x => x.Id)); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(userGroup1); + Remove(userGroup2); + } + + [Fact] + public async Task ShouldReturnAscendingOrderByName_WhenSearchTermIsProvided() + { + // Arrange + var userGroup1 = CreateUserGroup(Array.Empty()); + var userGroup2 = CreateUserGroup(Array.Empty()); + var userGroup3 = CreateUserGroup(Array.Empty()); + userGroup3.Name = userGroup1.Name + "2"; + + await AddAsync(userGroup1); + await AddAsync(userGroup2); + await AddAsync(userGroup3); + var query = new GetAllUserGroupsPaginated.Query() + { + SearchTerm = userGroup1.Name + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup3 }) + .OrderBy(x => x.Id)); + result.Items.Should().BeInAscendingOrder(x => x.Id); + + // Cleanup + Remove(userGroup1); + Remove(userGroup2); + Remove(userGroup3); + } + + [Fact] + public async Task ShouldReturnSortByInAscendingOrder_WhenCorrectSortByIsProvided() + { + // Arrange + var userGroup1 = CreateUserGroup(Array.Empty()); + var userGroup2 = CreateUserGroup(Array.Empty()); + + await AddAsync(userGroup1); + await AddAsync(userGroup2); + var query = new GetAllUserGroupsPaginated.Query() + { + SortBy = "Name" + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) + .OrderBy(x => x.Name)); + result.Items.Should().BeInAscendingOrder(x => x.Name); + + // Cleanup + Remove(userGroup1); + Remove(userGroup2); + } + + [Fact] + public async Task ShouldReturnInDescendingOrderOfId_WhenDescendingSortOrderIsProvided() + { + // Arrange + var userGroup1 = CreateUserGroup(Array.Empty()); + var userGroup2 = CreateUserGroup(Array.Empty()); + + await AddAsync(userGroup1); + await AddAsync(userGroup2); + var query = new GetAllUserGroupsPaginated.Query() + { + SortOrder = "desc" + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.TotalCount.Should().Be(2); + result.Items.Should() + .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) + .OrderByDescending(x => x.Id)); + result.Items.Should().BeInDescendingOrder(x => x.Id); + + // Cleanup + Remove(userGroup1); + Remove(userGroup2); + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs b/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs new file mode 100644 index 00000000..9053d204 --- /dev/null +++ b/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs @@ -0,0 +1,53 @@ +using Application.UserGroups.Queries; +using Domain.Entities; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Integration.UserGroups.Queries; + +public class GetUserGroupByIdTests : BaseClassFixture +{ + public GetUserGroupByIdTests(CustomApiFactory apiFactory) : base(apiFactory) + { + } + + [Fact] + public async Task ShouldReturnUserGroup_WhenThatUserGroupExists() + { + // Arrange + var userGroup = CreateUserGroup(Array.Empty()); + + await AddAsync(userGroup); + + var query = new GetUserGroupById.Query() + { + UserGroupId = userGroup.Id, + }; + + // Act + var result = await SendAsync(query); + + // Assert + result.Name.Should().Be(userGroup.Name); + + // Cleanup + Remove(userGroup); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() + { + // Arrange + var query = new GetUserGroupById.Query() + { + UserGroupId = Guid.NewGuid(), + }; + + // Act + var action = async () => await SendAsync(query); + + // Assert + await action.Should().ThrowAsync() + .WithMessage("User group does not exist."); + } +} \ No newline at end of file From 9baa635ce532d585fe27ea4b5c3a78ab765a1a49 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 25 Jun 2023 09:37:46 +0700 Subject: [PATCH 068/162] add: seed staff data (#262) --- .../Persistence/ApplicationDbContextSeed.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index 394444b2..c0c7a2c1 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -1,6 +1,7 @@ using Application.Helpers; using Application.Identity; using Domain.Entities; +using Domain.Entities.Physical; using Infrastructure.Shared; using Microsoft.Extensions.Configuration; using NodaTime; @@ -49,7 +50,7 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp }; salt = StringUtil.RandomSalt(); - var staff = new User() + var staffUser = new User() { Username = "staff", Email = "staff@profile.dev", @@ -77,6 +78,12 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp Created = LocalDateTime.FromDateTime(DateTime.UtcNow), Role = IdentityData.Roles.Employee, }; + + var staff = new Staff() + { + User = staffUser, + Room = null, + }; if (context.Departments.All(u => u.Name != department.Name)) { @@ -105,10 +112,14 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = itDepartment; await context.Users.AddAsync(employee); } - if (context.Users.All(u => u.Username != staff.Username)) + if (context.Users.All(u => u.Username != staffUser.Username)) { - staff.Department = department; - await context.Users.AddAsync(staff); + staffUser.Department = itDepartment; + await context.Users.AddAsync(staffUser); + } + if (context.Staffs.All(s => s.Id != staff.Id)) + { + await context.Staffs.AddAsync(staff); } } else @@ -119,10 +130,14 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = departmentEntity; await context.Users.AddAsync(employee); } - if (context.Users.All(u => u.Username != staff.Username)) + if (context.Users.All(u => u.Username != staffUser.Username)) + { + staffUser.Department = itDepartment; + await context.Users.AddAsync(staffUser); + } + if (context.Staffs.All(s => s.Id != staff.Id)) { - staff.Department = departmentEntity; - await context.Users.AddAsync(staff); + await context.Staffs.AddAsync(staff); } } From 438d6cbcd0de747d2ceb4049da69c049c18cbb99 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Mon, 26 Jun 2023 19:38:57 +0700 Subject: [PATCH 069/162] remove role constraint for getting document types (#279) * remove role constraint for getting document types * Update DocumentsController.cs --------- Co-authored-by: Khanh Nguyen --- src/Api/Controllers/DocumentsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2d500422..882bf6f2 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -122,7 +122,7 @@ public async Task>>> GetAllForEmp /// Get all document types /// /// A list of document types - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("types")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -302,4 +302,4 @@ public async Task>>> GetAllLog var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } -} \ No newline at end of file +} From 9ffe74fede427cd591b258347349361708e74791 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Mon, 26 Jun 2023 21:42:15 +0700 Subject: [PATCH 070/162] fix: assign document (#281) - did not update folder's number of documents - add new import request status: Assigned --- src/Application/ImportRequests/Commands/AssignDocument.cs | 8 ++++++-- src/Domain/Statuses/ImportRequestStatus.cs | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index 8b8f1fb0..9cdab838 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -77,6 +77,9 @@ public async Task Handle(Command request, CancellationToken ca importRequest.Document.Folder = folder; importRequest.Document.LastModified = localDateTimeNow; importRequest.Document.LastModifiedBy = request.CurrentUser.Id; + importRequest.Status = ImportRequestStatus.Assigned; + + folder.NumberOfDocuments += 1; var log = new DocumentLog() { @@ -94,11 +97,12 @@ public async Task Handle(Command request, CancellationToken ca UserId = request.CurrentUser.Id, Action = FolderLogMessage.AssignDocument, }; - _context.Documents.Update(importRequest.Document); + var result = _context.Documents.Update(importRequest.Document); + _context.Folders.Update(folder); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.FolderLogs.AddAsync(folderLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(importRequest); + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/src/Domain/Statuses/ImportRequestStatus.cs b/src/Domain/Statuses/ImportRequestStatus.cs index 65a65ffc..43c221dd 100644 --- a/src/Domain/Statuses/ImportRequestStatus.cs +++ b/src/Domain/Statuses/ImportRequestStatus.cs @@ -5,5 +5,6 @@ public enum ImportRequestStatus Pending, Approved, Rejected, + Assigned, CheckedIn, } \ No newline at end of file From a7676efe08d6da916178a152dc3f6c11c4aad944 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:21:23 +0700 Subject: [PATCH 071/162] Fix: assign staff + get document by id (#282) * fix: get document by id + assign staff * add: final --- src/Api/Controllers/DepartmentsController.cs | 4 ++-- src/Api/Controllers/StaffsController.cs | 2 ++ .../Documents/Queries/GetDocumentById.cs | 3 +++ .../Rooms/Queries/GetRoomByDepartmentId.cs | 3 ++- src/Application/Rooms/Queries/GetRoomByStaffId.cs | 3 +++ src/Application/Staffs/Commands/AssignStaff.cs | 15 +++++++++++++++ .../Persistence/ApplicationDbContextSeed.cs | 4 ++-- 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 1292c8e1..0b425045 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -23,7 +23,7 @@ public DepartmentsController(ICurrentUserService currentUserService) } /// - /// Get back a room based on its department id + /// Get a department by it id /// /// id of the department to be retrieved /// A DepartmentDto of the retrieved department @@ -51,7 +51,7 @@ public async Task>> GetById( } /// - /// Get back a department based on its id + /// Get rooms based on department id /// /// id of the department to be retrieved /// A DepartmentDto of the retrieved department diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 12452352..19c196b7 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -54,8 +54,10 @@ public async Task>> GetById( public async Task>> GetRoomByStaffId( [FromRoute] Guid staffId) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetRoomByStaffId.Query() { + CurrentUser = currentUser, StaffId = staffId, }; var result = await Mediator.Send(query); diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index cfaa653d..9babe0a2 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -39,6 +39,9 @@ public QueryHandler( public async Task Handle(Query request, CancellationToken cancellationToken) { var document = await _context.Documents + .Include(x => x.Folder) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .Include(x => x.Department) .Include(x => x.Importer) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); diff --git a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs index 45e99e75..a301bc3f 100644 --- a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs +++ b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs @@ -1,4 +1,5 @@ using Application.Common.Interfaces; +using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; using AutoMapper; @@ -30,7 +31,7 @@ public async Task Handle(Query request, CancellationToken cancellationT var room = await _context.Rooms .Include(x => x.Department) .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.DepartmentId == request.DepartmentId, cancellationToken: cancellationToken); + .FirstOrDefaultAsync(x => x.DepartmentId == request.DepartmentId, cancellationToken); if (room is null) { diff --git a/src/Application/Rooms/Queries/GetRoomByStaffId.cs b/src/Application/Rooms/Queries/GetRoomByStaffId.cs index 7a0c4f90..737ac602 100644 --- a/src/Application/Rooms/Queries/GetRoomByStaffId.cs +++ b/src/Application/Rooms/Queries/GetRoomByStaffId.cs @@ -1,6 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,7 @@ public class GetRoomByStaffId { public record Query : IRequest { + public User CurrentUser { get; init; } = null!; public Guid StaffId { get; init; } } diff --git a/src/Application/Staffs/Commands/AssignStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs index 3e569701..7627616a 100644 --- a/src/Application/Staffs/Commands/AssignStaff.cs +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -47,6 +47,7 @@ public async Task Handle(Command request, CancellationToken cancellati var room = await _context.Rooms .Include(x => x.Staff) + .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) @@ -64,6 +65,20 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("Room already has a staff."); } + var user = await _context.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == staff.Id, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + if (room.Department.Id != user.Department!.Id) + { + throw new ConflictException("Room is in different department."); + } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); staff.Room = room; diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index c0c7a2c1..f224fd2b 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -117,7 +117,7 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp staffUser.Department = itDepartment; await context.Users.AddAsync(staffUser); } - if (context.Staffs.All(s => s.Id != staff.Id)) + if (context.Staffs.All(s => s.User.Username != staff.User.Username)) { await context.Staffs.AddAsync(staff); } @@ -135,7 +135,7 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp staffUser.Department = itDepartment; await context.Users.AddAsync(staffUser); } - if (context.Staffs.All(s => s.Id != staff.Id)) + if (context.Staffs.All(s => s.User.Username != staff.User.Username)) { await context.Staffs.AddAsync(staff); } From 1b5ea901f9da33ad0046858225a339798920e9f6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:58:23 +0700 Subject: [PATCH 072/162] update: get all rooms now has availability (#280) --- .../Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs | 1 + src/Api/Controllers/RoomsController.cs | 1 + src/Application/Rooms/Queries/GetAllRoomsPaginated.cs | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 3bf5e63a..83ff67a1 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -9,5 +9,6 @@ public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } + public bool? IsAvailable { get; set; } public Guid? DepartmentId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 1f1dadca..adac1f99 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -65,6 +65,7 @@ public async Task>>> GetAllPaginated( { CurrentUser = currentUser, DepartmentId = queryParameters.DepartmentId, + IsAvailable = queryParameters.IsAvailable, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index b2961b8b..fa6c7615 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -18,6 +18,7 @@ public record Query : IRequest> { public User CurrentUser { get; init; } = null!; public Guid? DepartmentId { get; init; } + public bool? IsAvailable { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -54,6 +55,11 @@ public async Task> Handle(Query request, CancellationToke rooms = rooms.Where(x => x.Department.Id == request.DepartmentId); } + if (request.IsAvailable is not null) + { + rooms = rooms.Where(x => x.IsAvailable == request.IsAvailable); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { rooms = rooms.Where(x => From da653a22b3f5a21d31c0bfca4b058da16261b1a6 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Tue, 27 Jun 2023 19:30:11 +0700 Subject: [PATCH 073/162] Fix/import request (#288) * fix(ImportRequest): change business * fix: remove documment when reject --- .../Commands/ApproveOrRejectDocument.cs | 1 + .../Commands/RequestImportDocument.cs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index e428ad20..e3ab0367 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -119,6 +119,7 @@ public async Task Handle(Command request, CancellationToken ca if (request.Decision.IsRejection()) { importRequest.Status = ImportRequestStatus.Rejected; + _context.Documents.Remove(importRequest.Document); log.Action = DocumentLogMessages.Import.Reject; requestLog.Action = RequestLogMessages.RejectImport; } diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index d27cbeef..82ec398b 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -41,10 +41,16 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimePr public async Task Handle(Command request, CancellationToken cancellationToken) { - var document = _context.Documents.FirstOrDefault(x => - x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) - && x.Importer!.Id == request.Issuer.Id); - if (document is not null) + var documentRequest = await _context.ImportRequests + .Include(x => x.Document) + .ThenInclude(x => x.Importer) + .FirstOrDefaultAsync( x => + x.Document.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) + && x.Document.Importer!.Id == request.Issuer.Id + && x.Status != ImportRequestStatus.Rejected + , cancellationToken); + + if (documentRequest is not null) { throw new ConflictException($"Document title already exists for user {request.Issuer.FirstName}."); } From 3853ebdff4388d235c99e2ae4586047c0ae23f81 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:33:14 +0700 Subject: [PATCH 074/162] fix(ImportDocument): add isPrivate to controller (#289) --- src/Api/Controllers/DocumentsController.cs | 1 + .../Payload/Requests/Documents/ImportDocumentRequest.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 882bf6f2..e8c85eb7 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -162,6 +162,7 @@ public async Task>> Import( DocumentType = request.DocumentType, FolderId = request.FolderId, ImporterId = request.ImporterId, + IsPrivate = request.IsPrivate, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs index 0bbb2723..49cea0fe 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs @@ -25,4 +25,8 @@ public class ImportDocumentRequest /// Id of the folder that this document will be in /// public Guid FolderId { get; set; } + /// + /// + /// + public bool IsPrivate { get; set; } } \ No newline at end of file From 79dda82c28eefa0cfcfee4e88868b0e485cd24fd Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 28 Jun 2023 22:19:16 +0700 Subject: [PATCH 075/162] Refactor/logging (#296) * add: logging base * add: replace logging part 1 * final: finish logging --------- Co-authored-by: Nguyen Quang Chien Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- src/Api/Api.csproj | 2 + src/Api/Controllers/BorrowsController.cs | 7 +- src/Api/Controllers/DocumentsController.cs | 24 - src/Api/Controllers/FoldersController.cs | 25 - src/Api/Controllers/LockersController.cs | 30 +- src/Api/Controllers/LogsController.cs | 34 + .../GetAllLogsPaginatedQueryParameters.cs | 7 +- src/Api/Controllers/RoomsController.cs | 25 - src/Api/Controllers/UsersController.cs | 26 - src/Api/Extensions/HostExtensions.cs | 1 - src/Api/appsettings.Development.json | 46 +- src/Application/Application.csproj | 1 + .../Borrows/BorrowLogExtensions.cs | 28 + .../Commands/ApproveOrRejectBorrowRequest.cs | 16 +- .../Borrows/Commands/BorrowDocument.cs | 12 +- .../Borrows/Commands/CancelBorrowRequest.cs | 12 +- .../Borrows/Commands/CheckoutDocument.cs | 10 +- .../Borrows/Commands/ReturnDocument.cs | 12 +- .../Borrows/Commands/UpdateBorrow.cs | 13 +- .../Queries/GetAllRequestLogsPaginated.cs | 43 +- .../Common/Extensions/QueryableExtensions.cs | 28 +- .../Interfaces/IApplicationDbContext.cs | 5 +- src/Application/Common/Logging/EventId.cs | 11 + src/Application/Common/Logging/Logging.cs | 27 + .../Common/Messages/DocumentLogMessages.cs | 34 +- .../Common/Messages/FolderLogMessage.cs | 8 +- .../Common/Messages/LockerLogMessage.cs | 6 +- .../Common/Messages/RequestLogMessages.cs | 11 +- .../Common/Messages/RoomLogMessage.cs | 6 +- .../Common/Messages/UserLogMessages.cs | 10 +- src/Application/Common/Models/Dtos/LogDto.cs | 16 + src/Application/Common/Models/Log.cs | 19 + .../Documents/Commands/DeleteDocument.cs | 13 +- .../Documents/Commands/ImportDocument.cs | 15 +- .../Documents/Commands/ShareDocument.cs | 58 +- .../Documents/Commands/UpdateDocument.cs | 12 +- .../Documents/DocumentLogExtensions.cs | 24 + .../Queries/GetAllDocumentLogsPaginated.cs | 62 - src/Application/Folders/Commands/AddFolder.cs | 16 +- .../Folders/Commands/RemoveFolder.cs | 15 +- .../Folders/Commands/UpdateFolder.cs | 15 +- src/Application/Folders/FolderLogExtension.cs | 17 + .../Queries/GetAllFolderLogsPaginated.cs | 60 - .../Commands/ApproveOrRejectDocument.cs | 25 +- .../ImportRequests/Commands/AssignDocument.cs | 20 +- .../Commands/CheckinDocument.cs | 15 +- .../Commands/RequestImportDocument.cs | 16 +- .../ImportRequestLogExtensions.cs | 42 + src/Application/Lockers/Commands/AddLocker.cs | 17 +- .../Lockers/Commands/RemoveLocker.cs | 11 +- .../Lockers/Commands/UpdateLocker.cs | 15 +- src/Application/Lockers/LockerLogExtension.cs | 17 + .../Queries/GetAllLockerLogsPaginated.cs | 93 -- .../Lockers/Queries/GetAllLockersPaginated.cs | 19 +- .../Loggings/Queries/GetAllLogsPaginated.cs | 79 ++ src/Application/Rooms/Commands/AddRoom.cs | 18 +- src/Application/Rooms/Commands/RemoveRoom.cs | 17 +- src/Application/Rooms/Commands/UpdateRoom.cs | 17 +- .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 61 - src/Application/Rooms/RoomLogExtension.cs | 16 + .../Staffs/Commands/AssignStaff.cs | 13 +- .../Staffs/Commands/RemoveStaffFromRoom.cs | 13 +- src/Application/Staffs/StaffLogExtensions.cs | 13 + src/Application/Users/Commands/AddUser.cs | 20 +- src/Application/Users/Commands/UpdateUser.cs | 10 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 61 - src/Application/Users/UserLogExtensions.cs | 13 + .../Persistence/ApplicationDbContext.cs | 5 +- .../Persistence/ApplicationDbContextSeed.cs | 5 +- .../20230624100226_TrueLogging.Designer.cs | 1105 +++++++++++++++++ .../Migrations/20230624100226_TrueLogging.cs | 44 + .../ApplicationDbContextModelSnapshot.cs | 37 + 72 files changed, 2074 insertions(+), 625 deletions(-) create mode 100644 src/Api/Controllers/LogsController.cs create mode 100644 src/Application/Borrows/BorrowLogExtensions.cs create mode 100644 src/Application/Common/Logging/EventId.cs create mode 100644 src/Application/Common/Logging/Logging.cs create mode 100644 src/Application/Common/Models/Dtos/LogDto.cs create mode 100644 src/Application/Common/Models/Log.cs create mode 100644 src/Application/Documents/DocumentLogExtensions.cs delete mode 100644 src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs create mode 100644 src/Application/Folders/FolderLogExtension.cs delete mode 100644 src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs create mode 100644 src/Application/ImportRequests/ImportRequestLogExtensions.cs create mode 100644 src/Application/Lockers/LockerLogExtension.cs delete mode 100644 src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs create mode 100644 src/Application/Loggings/Queries/GetAllLogsPaginated.cs delete mode 100644 src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs create mode 100644 src/Application/Rooms/RoomLogExtension.cs create mode 100644 src/Application/Staffs/StaffLogExtensions.cs delete mode 100644 src/Application/Users/Queries/GetAllUserLogsPaginated.cs create mode 100644 src/Application/Users/UserLogExtensions.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.cs diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 11e16ecd..d45ec98c 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 0388c57a..de1c07ff 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -2,10 +2,9 @@ using Api.Controllers.Payload.Requests.Borrows; using Application.Borrows.Commands; using Application.Borrows.Queries; -using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; +using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Infrastructure.Identity.Authorization; @@ -239,7 +238,7 @@ public async Task>> Cancel( [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllRequestLogs( + public async Task>>> GetAllRequestLogs( [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { var query = new GetAllRequestLogsPaginated.Query() @@ -249,6 +248,6 @@ public async Task>>> GetAllRequ Size = queryParameters.Size, }; var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); + return Ok(Result>.Succeed(result)); } } \ No newline at end of file diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index e8c85eb7..1e88f100 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,9 +1,7 @@ -using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Documents; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; @@ -281,26 +279,4 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - - /// - /// Get all log of document - /// - /// - /// Paginated list of DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllDocumentLogsPaginated.Query() - { - DocumentId = queryParameters.ObjectId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 153367ab..5114d8e1 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,9 +1,7 @@ -using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Folders; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands; using Application.Folders.Queries; @@ -164,27 +162,4 @@ public async Task>> Update( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// - /// - /// - /// - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllFolderLogsPaginated.Query() - { - FolderId = queryParameters.ObjectId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index fb87d0ee..c48dcc9a 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -2,6 +2,7 @@ using Api.Controllers.Payload.Requests.Lockers; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; @@ -58,10 +59,12 @@ public async Task>> GetById( public async Task>>> GetAllPaginated( [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) { + var currentUserId = _currentUserService.GetId(); var currentUserRole = _currentUserService.GetRole(); var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllLockersPaginated.Query() { + CurrentUserId = currentUserId, CurrentUserRole = currentUserRole, CurrentUserDepartmentId = currentUserDepartmentId, RoomId = queryParameters.RoomId, @@ -154,31 +157,4 @@ public async Task>> Update( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Get all logs related to locker - /// - /// Query parameters - /// A list of LockerLogsDtos - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var currentUserRole = _currentUserService.GetRole(); - var currentUserDepartmentId = _currentUserService.GetDepartmentId(); - var query = new GetAllLockerLogsPaginated.Query() - { - CurrentUserRole = currentUserRole, - CurrentUserDepartmentId = currentUserDepartmentId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - LockerId = queryParameters.ObjectId - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } diff --git a/src/Api/Controllers/LogsController.cs b/src/Api/Controllers/LogsController.cs new file mode 100644 index 00000000..92d7560e --- /dev/null +++ b/src/Api/Controllers/LogsController.cs @@ -0,0 +1,34 @@ +using Api.Controllers.Payload.Requests; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using Application.Identity; +using Application.Logs.Queries; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class LogsController : ApiControllerBase +{ + /// + /// Get logs paginated + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet] + public async Task>> GetAllPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllLogsPaginated.Query() + { + ObjectId = queryParameters.ObjectId, + ObjectType = queryParameters.ObjectType, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index 59019d0d..68917040 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -17,9 +17,12 @@ public class GetAllLogsPaginatedQueryParameters /// Size number /// public int? Size { get; set; } - /// - /// User Id + /// Object Id /// public Guid? ObjectId { get; set; } + /// + /// Object type + /// + public string ObjectType { get; set; } = null!; } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index adac1f99..a285a3fd 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,9 +1,6 @@ -using Api.Controllers.Payload.Requests; -using Api.Controllers.Payload.Requests.Lockers; using Api.Controllers.Payload.Requests.Rooms; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Rooms.Commands; @@ -205,26 +202,4 @@ public async Task>> Update( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Get all room logs paginated - /// - /// - /// A paginated list of RoomLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllRoomLogsPaginated.Query() - { - RoomId = queryParameters.ObjectId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 12ca4695..aed6dfa8 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,13 +1,10 @@ -using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Users; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; using Application.Identity; using Application.Users.Commands; using Application.Users.Queries; using Infrastructure.Identity.Authorization; -using MediatR; using Microsoft.AspNetCore.Mvc; namespace Api.Controllers; @@ -188,27 +185,4 @@ public async Task>> UpdateSelf( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Get all user related logs paginated - /// - /// Get all users related logs query parameters - /// A paginated list of UserLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllUserLogsPaginated.Query() - { - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - UserId = queryParameters.ObjectId, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Extensions/HostExtensions.cs b/src/Api/Extensions/HostExtensions.cs index db1d0189..c736627e 100644 --- a/src/Api/Extensions/HostExtensions.cs +++ b/src/Api/Extensions/HostExtensions.cs @@ -23,7 +23,6 @@ public static IHost MigrateDatabase(this IHost host, Action + diff --git a/src/Application/Borrows/BorrowLogExtensions.cs b/src/Application/Borrows/BorrowLogExtensions.cs new file mode 100644 index 00000000..04df6443 --- /dev/null +++ b/src/Application/Borrows/BorrowLogExtensions.cs @@ -0,0 +1,28 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; + +namespace Application.Borrows; + +public static partial class BorrowLogExtensions +{ + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.NewBorrowRequest, EventId = Common.Logging.EventId.Add)] + public static partial void LogBorrowDocument(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Cancel, EventId = Common.Logging.EventId.OtherRequestRelated)] + public static partial void LogCancelBorrowRequest(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Checkout, EventId = Common.Logging.EventId.OtherRequestRelated)] + public static partial void LogCheckoutDocument(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Return, EventId = Common.Logging.EventId.OtherRequestRelated)] + public static partial void LogReturnDocument(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Return, EventId = Common.Logging.EventId.Update)] + public static partial void LogUpdateBorrow(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Approve, EventId = Common.Logging.EventId.Approve)] + public static partial void LogApproveBorrowRequest(this ILogger logger, string documentId, string borrowId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Borrow.Reject, EventId = Common.Logging.EventId.Reject)] + public static partial void LogRejectBorrowRequest(this ILogger logger, string documentId, string borrowId); +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index 45120059..a93e4e8d 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -9,6 +10,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -28,12 +30,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -133,6 +137,11 @@ is BorrowRequestStatus.Approved } borrowRequest.Status = BorrowRequestStatus.Approved; + + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) + { + _logger.LogApproveBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } } if (request.Decision.IsRejection()) @@ -140,6 +149,11 @@ is BorrowRequestStatus.Approved borrowRequest.Status = BorrowRequestStatus.Rejected; log.Action = DocumentLogMessages.Borrow.Reject; requestLog.Action = RequestLogMessages.RejectBorrow; + + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) + { + _logger.LogRejectBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } } borrowRequest.StaffReason = request.StaffReason; diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 7fbf04d7..873b5997 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; @@ -12,6 +13,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -51,13 +53,15 @@ public class CommandHandler : IRequestHandler private readonly IMapper _mapper; private readonly IPermissionManager _permissionManager; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _permissionManager = permissionManager; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -164,11 +168,13 @@ is BorrowRequestStatus.Approved Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.NewBorrowRequest, }; - var result = await _context.Borrows.AddAsync(entity, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - + using (Logging.PushProperties("Request", document.Id, user.Id)) + { + _logger.LogBorrowDocument(document.Id.ToString(), result.Entity.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index b4015ffd..e20f15ba 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -7,6 +8,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -24,12 +26,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -63,13 +67,17 @@ public async Task Handle(Command request, CancellationToken cancellat UserId = currentUser!.Id, User = currentUser, Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.CanCel, + Action = DocumentLogMessages.Borrow.Cancel, }; borrowRequest.Status = BorrowRequestStatus.Cancelled; var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) + { + _logger.LogCancelBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 9ac4d77b..f7d3f2a9 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -8,6 +9,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -25,12 +27,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -94,6 +98,10 @@ public async Task Handle(Command request, CancellationToken cancellat _context.Documents.Update(borrowRequest.Document); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentStaff.Id)) + { + _logger.LogCheckoutDocument(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index e18b5c89..86332e27 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -8,6 +9,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -25,12 +27,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + private readonly ILogger _logger; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -95,6 +99,10 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.DocumentLogs.AddAsync(log, cancellationToken); _context.Documents.Update(borrowRequest.Document); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) + { + _logger.LogReturnDocument(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index c8cd900f..7bbda719 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -10,6 +11,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Borrows.Commands; @@ -48,12 +50,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -115,12 +119,17 @@ is BorrowRequestStatus.Approved User = request.CurrentUser, ObjectId = borrowRequest.Document.Id, Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.Update, + Action = DocumentLogMessages.Borrow.UpdateBorrow, }; var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) + { + _logger.LogUpdateBorrow(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + } + return _mapper.Map(result.Entity); } } diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs index dc3176e8..cb4b4b3b 100644 --- a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -1,7 +1,10 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Logging; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; using AutoMapper; using Domain.Enums; using MediatR; @@ -11,14 +14,15 @@ namespace Application.Borrows.Queries; public class GetAllRequestLogsPaginated { - public record Query : IRequest> + public record Query : IRequest> { + public Guid? BorrowRequestId { get; set; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } } - public class QueryHandler : IRequestHandler> + public class QueryHandler : IRequestHandler> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -29,30 +33,47 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Query request, CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { - var logs = _context.RequestLogs - .Include(x => x.ObjectId) + var logs = _context.Logs + .Where(x => x.ObjectType!.Equals("BorrowRequest")) .AsQueryable(); + if (request.BorrowRequestId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.BorrowRequestId); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { - logs = logs.Where(x => - x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + logs = logs.Where(x => + x.Message!.ToLower().Contains(request.SearchTerm.Trim().ToLower())); } var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + var sizeNumber = request.Size is null or <= 0 ? 10 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs - .OrderByDescending(x => x.Time) + .OrderByDescending(x => x.Time!) .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); - var result = _mapper.Map>(list); + var result = list.Select(x => new LogDto() + { + Id = x.Id, + Event = x.Event, + Template = x.Template, + Message = x.Message, + Level = x.Level, + ObjectType = x.ObjectType, + User = _mapper.Map(_context.Users.FirstOrDefault(y => y.Id == x.UserId!)), + ObjectId = x.ObjectId!, + Time = x.Time?.ToDateTimeUtc(), + }) + .ToList(); - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 71587465..351a92be 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -1,9 +1,12 @@ using System.Linq.Expressions; +using Application.Common.Interfaces; using Application.Common.Mappings; using Application.Common.Models; using Application.Common.Models.Dtos; +using Application.Users.Queries; using AutoMapper; using Domain.Common; +using Domain.Entities; using Microsoft.EntityFrameworkCore; namespace Application.Common.Extensions; @@ -32,31 +35,6 @@ public static IQueryable Paginate(this IQueryable ite return items.Skip((page - 1) * size).Take(size); } - public static async Task> LoggingListPaginateAsync( - this IQueryable items, - int? page, - int? size, - IConfigurationProvider mapperConfiguration, - CancellationToken cancellationToken) - where TEntityDto : BaseDto, IMapFrom - where TLoggingEntity : BaseLoggingEntity - where TEntity : BaseEntity - { - var pageNumber = page is null or <= 0 ? 1 : page; - var sizeNumber = size is null or <= 0 ? 10 : size; - - var count = await items.CountAsync(cancellationToken); - var list = await items - .OrderByDescending(x => x.Time) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var mapper = mapperConfiguration.CreateMapper(); - var result = mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); - } - public static async Task> ListPaginateWithSortAsync( this IQueryable items, int? page, diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 9176b655..fa550c14 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,3 +1,4 @@ +using Application.Common.Models; using Domain.Entities; using Domain.Entities.Digital; using Domain.Entities.Logging; @@ -18,12 +19,14 @@ public interface IApplicationDbContext public DbSet Documents { get; } public DbSet ImportRequests { get; } public DbSet Borrows { get; } - public DbSet Permissions { get; } + public DbSet Permissions { get; } public DbSet UserGroups { get; } public DbSet Files { get; } public DbSet Entries { get; } + public DbSet Logs { get; } + public DbSet RoomLogs { get; } public DbSet LockerLogs { get; } public DbSet FolderLogs { get; } diff --git a/src/Application/Common/Logging/EventId.cs b/src/Application/Common/Logging/EventId.cs new file mode 100644 index 00000000..9846cf9b --- /dev/null +++ b/src/Application/Common/Logging/EventId.cs @@ -0,0 +1,11 @@ +namespace Application.Common.Logging; + +public static class EventId { + public const int Add = 227910; + public const int Remove = 177013; + public const int Update = 366224; + public const int Approve = 372415; + public const int Reject = 342738; + public const int OtherRequestRelated = 1337; + public const int Other = 390009; +} \ No newline at end of file diff --git a/src/Application/Common/Logging/Logging.cs b/src/Application/Common/Logging/Logging.cs new file mode 100644 index 00000000..15222cdd --- /dev/null +++ b/src/Application/Common/Logging/Logging.cs @@ -0,0 +1,27 @@ +using Serilog.Context; + +namespace Application.Common.Logging; + +public class Logging : IDisposable +{ + private IDisposable ObjectType { get; } + private IDisposable ObjectId { get; } + private IDisposable UserId { get; } + + private Logging(string objectType, Guid objectId, Guid userId) + { + ObjectType = LogContext.PushProperty("ObjectType", objectType); + ObjectId = LogContext.PushProperty("ObjectId", objectId); + UserId = LogContext.PushProperty("UserId", userId); + } + + public static Logging PushProperties(string objectType, Guid objectId, Guid userId) + => new(objectType, objectId, userId); + public void Dispose() + { + ObjectType.Dispose(); + ObjectId.Dispose(); + UserId.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index e92040f2..44c8eb1f 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -4,27 +4,25 @@ public static class DocumentLogMessages { public static class Import { - public const string NewImport = "Imported new document"; + public const string NewImport = "Imported new document with id {DocumentId}"; public const string NewImportRequest = "Created new import request"; - public const string Checkin = "Checked in document"; - public const string Approve = "Document is approved to be imported"; - public const string Reject = "Rejected import request"; - public const string Assign = "Assigned to a folder"; + public const string Checkin = "Checked in document with id {DocumentId}"; + public const string Approve = "Document with id {DocumentId} is approved to be imported"; + public const string Reject = "Document with id {DocumentId} is rejected to be imported"; + public const string Assign = "Assigned document with id {DocumentId} to folder {FolderId}"; } public static class Borrow { - public const string NewBorrowRequest = "Created new borrow request"; - public const string CanCel = "Cancelled borrow request"; - public const string Approve = "Approved borrow request"; - public const string Reject = "Rejected borrow request"; - public const string Checkout = "Checked out borrow request"; - public const string Return = "Returned borrow request"; - public const string Update = "Updated borrow request"; + public const string NewBorrowRequest = "Create new borrow request for document {DocumentId} with id {BorrowId}"; + public const string Cancel = "Cancel borrow request for document {DocumentId} with id {BorrowId}"; + public const string Approve = "Approve borrow request for document {DocumentId} with id {BorrowId}"; + public const string Reject = "Reject borrow request for document {DocumentId} with id {BorrowId}"; + public const string Checkout = "Check out borrow request for document {DocumentId} with id {BorrowId}"; + public const string Return = "Return borrow request for document {DocumentId} with id {BorrowId}"; + public const string UpdateBorrow = "Update borrow request for document {DocumentId} with id {BorrowId}"; } - public const string Delete = "Delete document"; - public const string Update = "Updated document information"; - public static string GrantRead(string userName) => $"Share Read Permission to user {userName}"; - public static string GrantBorrow(string userName) => $"Share Borrow Permission to user {userName}"; - public static string RevokeRead(string userName) => $"Remove Read Permission to user {userName}"; - public static string RevokeBorrow(string userName) => $"Remove Borrow Permission to user {userName}"; + public const string Delete = "Delete document with id {DocumentId}"; + public const string Update = "Update document with id {DocumentId} "; + public const string Grant = "Share Permission {Permission} to user {Username}"; + public const string Revoke = "Remove Permission {Permission} from user {Username}"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs index c2bffb4d..ff1c0f06 100644 --- a/src/Application/Common/Messages/FolderLogMessage.cs +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -2,8 +2,8 @@ namespace Application.Common.Messages; public static class FolderLogMessage { - public const string Add = "Added folder"; - public const string Update = "Updated folder"; - public const string Remove = "Removed folder"; - public const string AssignDocument = "Assigned document to folder"; + public const string Add = "Add folder with Id {FolderId} to locker {LockerId} in room {RoomId} in department {DepartmentName}"; + public const string Update = "Updated folder with Id {FolderId} to locker {LockerId} in room {RoomId} in department {DepartmentName}"; + public const string Remove = "Removed folder with Id {FolderId} to locker {LockerId} in room {RoomId} in department {DepartmentName}"; + public const string AssignDocument = "New document with id {DocumentId} was assigned to this folder"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/LockerLogMessage.cs b/src/Application/Common/Messages/LockerLogMessage.cs index 291e4510..604229b7 100644 --- a/src/Application/Common/Messages/LockerLogMessage.cs +++ b/src/Application/Common/Messages/LockerLogMessage.cs @@ -2,7 +2,7 @@ namespace Application.Common.Messages; public static class LockerLogMessage { - public const string Add = "Added locker"; - public const string Update = "Updated locker"; - public const string Remove = "Removed locker"; + public const string Add = "Add locker with id {LockerId} to room {RoomId} in Department {DepartmentName}"; + public const string Update = "Update locker with id {LockerId} to room {RoomId} in Department {DepartmentName}"; + public const string Remove = "Remove locker with id {LockerId} to room {RoomId} in Department {DepartmentName}"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs index a84fbc04..2142c060 100644 --- a/src/Application/Common/Messages/RequestLogMessages.cs +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -2,9 +2,10 @@ namespace Application.Common.Messages; public static class RequestLogMessages { - public const string ApproveImport = "Approved import request"; - public const string RejectImport = "Rejected import request"; - public const string ApproveBorrow = "Rejected borrow request"; - public const string RejectBorrow = "Rejected borrow request"; - public const string CheckInImport = "Checkin import request"; + public const string AddImportRequest = "Created new import request with id {RequestId}"; + public const string ApproveImport = "Approved import request with id {RequestId}"; + public const string RejectImport = "Rejected import request with id {RequestId}"; + public const string ApproveBorrow = "Rejected borrow request with id {RequestId}"; + public const string RejectBorrow = "Rejected borrow request with id {RequestId}"; + public const string CheckInImport = "Checked in import request with id {RequestId}"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/RoomLogMessage.cs b/src/Application/Common/Messages/RoomLogMessage.cs index 90125821..926be34d 100644 --- a/src/Application/Common/Messages/RoomLogMessage.cs +++ b/src/Application/Common/Messages/RoomLogMessage.cs @@ -2,7 +2,7 @@ namespace Application.Common.Messages; public static class RoomLogMessage { - public const string Add = "Added room"; - public const string Update = "Updated room"; - public const string Remove = "Removed room"; + public const string Add = "Add room with Id {RoomId} in department {DepartmentName}"; + public const string Update = "Update room with Id {RoomId} in department {DepartmentName}"; + public const string Remove = "Remove room with Id {RoomId} in department {DepartmentName}"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/UserLogMessages.cs b/src/Application/Common/Messages/UserLogMessages.cs index 5599cb71..82182f20 100644 --- a/src/Application/Common/Messages/UserLogMessages.cs +++ b/src/Application/Common/Messages/UserLogMessages.cs @@ -2,15 +2,15 @@ namespace Application.Common.Messages; public static class UserLogMessages { - public static string Add(string role) => $"Added user with role {role}"; - public const string Update = "Updated user"; + public const string Add = "Added user {Username} with email {Email} of role {Role}"; + public const string Update = "Updated user {Username} information"; public const string Disable = "Disabled user"; public static class Staff { - public const string AddStaff = "Added a new staff"; - public static string AssignStaff(string roomId) => $"Assigned user to be staff of room {roomId}"; - public const string RemoveFromRoom = "Removed staff from room"; + public const string AddStaff = "Add a new staff with id {StaffId}"; + public const string AssignStaff = "Assign staff {StaffId} to room {RoomId}"; + public const string RemoveFromRoom = "Remove staff {StaffId} from room {RoomId}"; public const string Remove = "Removed staff"; } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/LogDto.cs b/src/Application/Common/Models/Dtos/LogDto.cs new file mode 100644 index 00000000..eeef15e1 --- /dev/null +++ b/src/Application/Common/Models/Dtos/LogDto.cs @@ -0,0 +1,16 @@ +using Application.Users.Queries; +using NodaTime; + +namespace Application.Common.Models.Dtos; + +public class LogDto { + public int Id { get; set; } + public string? Template { get; set; } + public string? Message { get; set; } + public string? Level { get; set; } + public DateTime? Time { get; set; } + public string? Event { get; set; } + public UserDto? User { get; set; } + public Guid? ObjectId { get; set; } + public string? ObjectType { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Log.cs b/src/Application/Common/Models/Log.cs new file mode 100644 index 00000000..ba3e6e61 --- /dev/null +++ b/src/Application/Common/Models/Log.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using NodaTime; + +namespace Application.Common.Models; + +public class Log { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + public string? Template { get; set; } + public string? Message { get; set; } + public string? Level { get; set; } + public Instant? Time { get; set; } + public string? Event { get; set; } + public Guid? UserId { get; set; } + public Guid? ObjectId { get; set; } + public string? ObjectType { get; set; } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index b5455f63..deeb88a9 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,11 +1,14 @@ using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Documents.Commands; @@ -23,12 +26,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -61,6 +66,12 @@ public async Task Handle(Command request, CancellationToken cancell var result = _context.Documents.Remove(document); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogDeleteDocument(result.Entity.Id.ToString()); + } + return _mapper.Map(result.Entity); } } diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index a60f64f2..4f2c19b9 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,8 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; +using Application.ImportRequests; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; @@ -10,6 +12,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Documents.Commands; @@ -33,12 +36,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -123,6 +128,14 @@ public async Task Handle(Command request, CancellationToken cancell _context.Folders.Update(folder); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogImportDocument(result.Entity.Id.ToString()); + } + using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) + { + _logger.LogAssignDocumentToFolder(result.Entity.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs index 7b40ad74..fbddc26b 100644 --- a/src/Application/Documents/Commands/ShareDocument.cs +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; @@ -9,6 +10,7 @@ using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Documents.Commands; @@ -31,13 +33,15 @@ public class CommandHandler : IRequestHandler private readonly IMapper _mapper; private readonly IPermissionManager _permissionManager; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider, ILogger logger) { _applicationDbContext = applicationDbContext; _mapper = mapper; _permissionManager = permissionManager; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -79,8 +83,8 @@ public async Task Handle(Command request, CancellationToken cancell Action = string.Empty, }; - await HandlePermissionGrantOrRevoke(request.CanRead, document, DocumentOperation.Read, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); - await HandlePermissionGrantOrRevoke(request.CanBorrow, document, DocumentOperation.Borrow, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); + await HandlePermissionGrantOrRevoke(request.CanRead, document, DocumentOperation.Read, user, request.ExpiryDate.ToLocalTime(), request.CurrentUser.Id, cancellationToken); + await HandlePermissionGrantOrRevoke(request.CanBorrow, document, DocumentOperation.Borrow, user, request.ExpiryDate.ToLocalTime(), request.CurrentUser.Id, cancellationToken); if (!string.IsNullOrEmpty(log.Action)) { @@ -96,19 +100,19 @@ private async Task HandlePermissionGrantOrRevoke( DocumentOperation operation, User user, DateTime expiryDate, - CancellationToken cancellationToken, - DocumentLog log) + Guid currentUserId, + CancellationToken cancellationToken) { var isGranted = _permissionManager.IsGranted(document.Id, operation, user.Id); if (canPerformAction && !isGranted) { - await GrantPermission(document, operation, user, expiryDate, log, cancellationToken); + await GrantPermission(document, operation, user, expiryDate, currentUserId, cancellationToken); } if (!canPerformAction && isGranted) { - await RevokePermission(document, operation, user, log, cancellationToken); + await RevokePermission(document, operation, user, currentUserId, cancellationToken); } } @@ -117,36 +121,52 @@ private async Task GrantPermission( DocumentOperation operation, User user, DateTime expiryDate, - DocumentLog log, + Guid currentUserId, CancellationToken cancellationToken) { await _permissionManager.GrantAsync(document, operation, new[] { user }, expiryDate, cancellationToken); // log - log.Action = operation switch + using (Logging.PushProperties(nameof(Document), document.Id, currentUserId)) { - DocumentOperation.Read => DocumentLogMessages.GrantRead(user.Username), - DocumentOperation.Borrow => DocumentLogMessages.GrantBorrow(user.Username), - _ => log.Action - }; + switch(operation) + { + case DocumentOperation.Read: + _logger.LogGrantPermission(DocumentOperation.Read.ToString(), user.Username); + break; + case DocumentOperation.Borrow: + _logger.LogGrantPermission(DocumentOperation.Borrow.ToString(), user.Username); + break; + default: + break; + } + } } private async Task RevokePermission( Document document, DocumentOperation operation, User user, - DocumentLog log, + Guid currentUserId, CancellationToken cancellationToken) { await _permissionManager.RevokeAsync(document.Id, operation, new[] { user.Id }, cancellationToken); // log - log.Action = operation switch + using (Logging.PushProperties(nameof(Document), document.Id, currentUserId)) { - DocumentOperation.Read => DocumentLogMessages.RevokeRead(user.Username), - DocumentOperation.Borrow => DocumentLogMessages.RevokeBorrow(user.Username), - _ => log.Action - }; + switch(operation) + { + case DocumentOperation.Read: + _logger.LogRevokePermission(DocumentOperation.Read.ToString(), user.Username); + break; + case DocumentOperation.Borrow: + _logger.LogRevokePermission(DocumentOperation.Borrow.ToString(), user.Username); + break; + default: + break; + } + } } } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index 195cc593..fe52ba89 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -10,6 +11,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Documents.Commands; @@ -50,12 +52,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -110,6 +114,12 @@ public async Task Handle(Command request, CancellationToken cancell var result = _context.Documents.Update(document); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogUpdateDocument(result.Entity.Id.ToString()); + } + return _mapper.Map(result.Entity); } diff --git a/src/Application/Documents/DocumentLogExtensions.cs b/src/Application/Documents/DocumentLogExtensions.cs new file mode 100644 index 00000000..790b5479 --- /dev/null +++ b/src/Application/Documents/DocumentLogExtensions.cs @@ -0,0 +1,24 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Documents; + +public static partial class DocumentLogExtensions +{ + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.NewImport, EventId = EventId.Add)] + public static partial void LogImportDocument(this ILogger logger, string documentId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Delete, EventId = EventId.Remove)] + public static partial void LogDeleteDocument(this ILogger logger, string documentId); + + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Update, EventId = EventId.Update)] + public static partial void LogUpdateDocument(this ILogger logger, string documentId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Grant, EventId = EventId.Update)] + public static partial void LogGrantPermission(this ILogger logger, string permission, string username); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Revoke, EventId = EventId.Update)] + public static partial void LogRevokePermission(this ILogger logger, string permission, string username); +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs deleted file mode 100644 index db36ee66..00000000 --- a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries; - -public class GetAllDocumentLogsPaginated -{ - public record Query : IRequest> - { - public Guid? DocumentId { get; set; } - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - } - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var logs = _context.DocumentLogs - .Include(x => x.ObjectId) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .AsQueryable(); - - if (request.DocumentId is not null) - { - logs = logs.Where(x => x.ObjectId! == request.DocumentId); - } - - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - logs = logs.Where(x => - x.Action.ToLower().Contains(request.SearchTerm.ToLower())); - } - - - return await logs - .LoggingListPaginateAsync( - request.Page, - request.Size, - _mapper.ConfigurationProvider, - cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 9eff4671..480a8a7c 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -11,6 +12,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Folders.Commands; @@ -54,18 +56,21 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) { var locker = await _context.Lockers .Include(x => x.Room) + .ThenInclude(y => y.Department) .FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); if (locker is null) @@ -117,6 +122,15 @@ public async Task Handle(Command request, CancellationToken cancellat _context.Lockers.Update(locker); await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Folder), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogAddFolder(result.Entity.Id.ToString(), + result.Entity.Locker.Id.ToString(), + locker.Room.Id.ToString(), + locker.Room.Department.Name); + } + return _mapper.Map(result.Entity); } diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index c6a0c6c7..a09af172 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -9,6 +10,7 @@ using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Folders.Commands; @@ -27,12 +29,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -77,6 +81,15 @@ public async Task Handle(Command request, CancellationToken cancellat locker.NumberOfFolders -= 1; await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) + { + _logger.LogRemoveFolder(folder.Id.ToString(), + folder.Locker.Id.ToString(), + folder.Locker.Room.Id.ToString(), + folder.Locker.Room.Department.Name); + } + return _mapper.Map(result.Entity); } diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index cec1d243..3abe34fa 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -10,6 +11,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Folders.Commands; @@ -50,12 +52,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -106,6 +110,15 @@ public async Task Handle(Command request, CancellationToken cancellat var result = _context.Folders.Update(folder); await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) + { + _logger.LogAddFolder(folder.Id.ToString(), + folder.Locker.Id.ToString(), + folder.Locker.Room.Id.ToString(), + folder.Locker.Room.Department.Name); + } + return _mapper.Map(result.Entity); } private async Task DuplicatedNameFolderExistsInSameLockerAsync( diff --git a/src/Application/Folders/FolderLogExtension.cs b/src/Application/Folders/FolderLogExtension.cs new file mode 100644 index 00000000..aca354d6 --- /dev/null +++ b/src/Application/Folders/FolderLogExtension.cs @@ -0,0 +1,17 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Folders; + +public static partial class FolderLogExtension { + + [LoggerMessage(Level = LogLevel.Information, Message = FolderLogMessage.Add, EventId = EventId.Add)] + public static partial void LogAddFolder(this ILogger logger, string folderId, string lockerId, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = FolderLogMessage.Remove, EventId = EventId.Remove)] + public static partial void LogRemoveFolder(this ILogger logger, string folderId, string lockerId, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = FolderLogMessage.Update, EventId = EventId.Update)] + public static partial void LogUpdateFolder(this ILogger logger, string folderId, string lockerId, string roomId, string departmentName); +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs deleted file mode 100644 index 537c2dc0..00000000 --- a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Queries; - -public class GetAllFolderLogsPaginated -{ - public record Query : IRequest> - { - public Guid? FolderId { get; init; } - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - } - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var logs = _context.FolderLogs - .Include(x => x.ObjectId) - .AsQueryable(); - - if (request.FolderId is not null) - { - logs = logs.Where(x => x.ObjectId! == request.FolderId); - } - - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - logs = logs.Where(x => - x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); - } - - - return await logs - .LoggingListPaginateAsync( - request.Page, - request.Size, - _mapper.ConfigurationProvider, - cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index e3ab0367..d6efabd9 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -1,15 +1,18 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.ImportRequests.Commands; @@ -40,12 +43,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -114,6 +119,15 @@ public async Task Handle(Command request, CancellationToken ca importRequest.Status = ImportRequestStatus.Approved; log.Action = DocumentLogMessages.Import.Approve; requestLog.Action = RequestLogMessages.ApproveImport; + + using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) + { + _logger.LogApproveImportRequestForDocument(document.Id.ToString()); + } + using (Logging.PushProperties(nameof(ImportRequest), importRequest.Id, request.CurrentUser.Id)) + { + _logger.LogApproveImportRequest(importRequest.Id.ToString()); + } } if (request.Decision.IsRejection()) @@ -122,6 +136,15 @@ public async Task Handle(Command request, CancellationToken ca _context.Documents.Remove(importRequest.Document); log.Action = DocumentLogMessages.Import.Reject; requestLog.Action = RequestLogMessages.RejectImport; + + using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) + { + _logger.LogRejectImportRequestForDocument(document.Id.ToString()); + } + using (Logging.PushProperties(nameof(ImportRequest), importRequest.Id, request.CurrentUser.Id)) + { + _logger.LogRejectImportRequest(importRequest.Id.ToString()); + } } importRequest.StaffReason = request.StaffReason; diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index 9cdab838..b01ff526 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -1,13 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.ImportRequests.Commands; @@ -27,17 +30,18 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) { - var importRequest = await _context.ImportRequests .Include(x => x.Document) .Include(x => x.Room) @@ -78,8 +82,10 @@ public async Task Handle(Command request, CancellationToken ca importRequest.Document.LastModified = localDateTimeNow; importRequest.Document.LastModifiedBy = request.CurrentUser.Id; importRequest.Status = ImportRequestStatus.Assigned; - + folder.NumberOfDocuments += 1; + folder.LastModified = localDateTimeNow; + folder.LastModifiedBy = request.CurrentUser.Id; var log = new DocumentLog() { @@ -102,6 +108,14 @@ public async Task Handle(Command request, CancellationToken ca await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.FolderLogs.AddAsync(folderLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Document), importRequest.Document.Id, request.CurrentUser.Id)) + { + _logger.LogAssignDocument(importRequest.Document.Id.ToString(), folder.Id.ToString()); + } + using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) + { + _logger.LogAssignDocumentToFolder(importRequest.Document.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index 70c0c34a..04c8b607 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -1,13 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.ImportRequests.Commands; @@ -25,12 +28,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -95,6 +100,14 @@ public async Task Handle(Command request, CancellationToken cancell await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.RequestLogs.AddAsync(importLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) + { + _logger.LogCheckinDocument(importRequest.Document.Id.ToString()); + } + using (Logging.PushProperties(nameof(ImportRequest), importRequest.Id, request.CurrentUser.Id)) + { + _logger.LogCheckinImportRequest(importRequest.Id.ToString()); + } return _mapper.Map(result.Entity); } diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 82ec398b..5c1e7894 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; @@ -9,6 +10,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.ImportRequests.Commands; @@ -31,12 +33,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -99,8 +103,16 @@ public async Task Handle(Command request, CancellationToken ca Action = DocumentLogMessages.Import.NewImportRequest, }; var result = await _context.ImportRequests.AddAsync(importRequest, cancellationToken); - await _context.DocumentLogs.AddAsync(log, cancellationToken); + var documentEntry = await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Document), documentEntry.Entity.Id, request.Issuer.Id)) + { + _logger.LogAddDocument(documentEntry.Entity.Id.ToString()); + } + using (Logging.PushProperties(nameof(ImportRequest), importRequest.Id, request.Issuer.Id)) + { + _logger.LogAddDocumentRequest(result.Entity.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/ImportRequests/ImportRequestLogExtensions.cs b/src/Application/ImportRequests/ImportRequestLogExtensions.cs new file mode 100644 index 00000000..6719dde6 --- /dev/null +++ b/src/Application/ImportRequests/ImportRequestLogExtensions.cs @@ -0,0 +1,42 @@ +using Application.Common.Messages; +using Domain.Entities.Physical; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.ImportRequests; + +public static partial class ImportRequestLogExtensions { + // Approve or reject document + [LoggerMessage(Level = LogLevel.Information, Message = RequestLogMessages.ApproveImport, EventId = EventId.Approve)] + public static partial void LogApproveImportRequest(this ILogger logger, string requestId); + + [LoggerMessage(Level = LogLevel.Information, Message = RequestLogMessages.RejectImport, EventId = EventId.Reject)] + public static partial void LogRejectImportRequest(this ILogger logger, string requestId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.Approve, EventId = EventId.Approve)] + public static partial void LogApproveImportRequestForDocument(this ILogger logger, string documentId); + + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.Reject, EventId = EventId.Reject)] + public static partial void LogRejectImportRequestForDocument(this ILogger logger, string documentId); + + // Assign document + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.Assign, EventId = EventId.Update)] + public static partial void LogAssignDocument(this ILogger logger, string documentId, string folderId); + + [LoggerMessage(Level = LogLevel.Information, Message = FolderLogMessage.AssignDocument, EventId = EventId.Update)] + public static partial void LogAssignDocumentToFolder(this ILogger logger, string documentId); + + // Check in document + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.Checkin, EventId = EventId.Update)] + public static partial void LogCheckinDocument(this ILogger logger, string documentId); + + [LoggerMessage(Level = LogLevel.Information, Message = RequestLogMessages.CheckInImport, EventId = EventId.Update)] + public static partial void LogCheckinImportRequest(this ILogger logger, string requestId); + + // Request import document + [LoggerMessage(Level = LogLevel.Information, Message = DocumentLogMessages.Import.NewImportRequest, EventId = EventId.Update)] + public static partial void LogAddDocument(this ILogger logger, string requestId); + + [LoggerMessage(Level = LogLevel.Information, Message = RequestLogMessages.AddImportRequest, EventId = EventId.Update)] + public static partial void LogAddDocumentRequest(this ILogger logger, string requestId); +} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index f1a9097c..3bc3c3f3 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -10,6 +11,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Lockers.Commands; @@ -51,17 +53,21 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) { - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var room = await _context.Rooms + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) { @@ -92,6 +98,8 @@ public async Task Handle(Command request, CancellationToken cancellat CreatedBy = request.CurrentUser.Id, }; + var result = await _context.Lockers.AddAsync(entity, cancellationToken); + var log = new LockerLog() { User = request.CurrentUser, @@ -100,11 +108,14 @@ public async Task Handle(Command request, CancellationToken cancellat Time = localDateTimeNow, Action = LockerLogMessage.Add, }; - var result = await _context.Lockers.AddAsync(entity, cancellationToken); room.NumberOfLockers += 1; _context.Rooms.Update(room); await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Locker), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogAddLocker(result.Entity.Id.ToString(), room.Id.ToString(), room.Department.Name); + } return _mapper.Map(result.Entity); } diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index c58f35ab..a9110258 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,13 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Lockers.Commands; @@ -36,12 +39,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -79,6 +84,10 @@ public async Task Handle(Command request, CancellationToken cancellat _context.Rooms.Update(room); await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Locker), locker.Id, request.CurrentUser.Id)) + { + _logger.LogRemoveLocker(locker.Id.ToString(), locker.Room.Id.ToString(), locker.Room.Department.Name); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index e618129a..49f1ad55 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -10,6 +11,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Lockers.Commands; @@ -47,12 +49,13 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + private readonly ILogger _logger; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -86,6 +89,8 @@ public async Task Handle(Command request, CancellationToken cancellat locker.LastModified = localDateTimeNow; locker.LastModifiedBy = request.CurrentUser.Id; + + var log = new LockerLog() { User = request.CurrentUser, @@ -97,6 +102,12 @@ public async Task Handle(Command request, CancellationToken cancellat var result = _context.Lockers.Update(locker); await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Locker), locker.Id, request.CurrentUser.Id)) + { + _logger.LogUpdateLocker(locker.Id.ToString(), locker.Room.Id.ToString(), locker.Room.Department.Name); + } + return _mapper.Map(result.Entity); } diff --git a/src/Application/Lockers/LockerLogExtension.cs b/src/Application/Lockers/LockerLogExtension.cs new file mode 100644 index 00000000..f38951ce --- /dev/null +++ b/src/Application/Lockers/LockerLogExtension.cs @@ -0,0 +1,17 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Lockers; + +public static partial class LockerLogExtension { + + [LoggerMessage(Level = LogLevel.Information, Message = LockerLogMessage.Add, EventId = EventId.Add)] + public static partial void LogAddLocker(this ILogger logger, string lockerId, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = LockerLogMessage.Remove, EventId = EventId.Remove)] + public static partial void LogRemoveLocker(this ILogger logger, string lockerId, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = LockerLogMessage.Update, EventId = EventId.Update)] + public static partial void LogUpdateLocker(this ILogger logger, string lockerId, string roomId, string departmentName); +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs deleted file mode 100644 index 6adb27be..00000000 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities; -using Domain.Entities.Logging; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Queries; - -public class GetAllLockerLogsPaginated -{ - public record Query : IRequest> - { - public string CurrentUserRole { get; init; } = null!; - public Guid CurrentUserDepartmentId { get; init; } - public string? SearchTerm { get; init; } - public Guid? LockerId { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - } - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - - if (request.CurrentUserRole.IsStaff()) - { - if (request.LockerId is null) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } - - var currentRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); - - if (currentRoom is null) - { - throw new UnauthorizedAccessException("User cannot access this resource"); - } - - if (!IsSameRoom(currentRoom.Id, request.LockerId.Value)) - { - throw new UnauthorizedAccessException("User cannot access this resource"); - } - } - - var logs = _context.LockerLogs - .Include(x => x.ObjectId) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .AsQueryable(); - - if (request.LockerId is not null) - { - logs = logs.Where(x => x.ObjectId! == request.LockerId); - } - - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - logs = logs.Where(x => - x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); - } - - return await logs - .LoggingListPaginateAsync( - request.Page, - request.Size, - _mapper.ConfigurationProvider, - cancellationToken); - } - - private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) - => await _context.Rooms.FirstOrDefaultAsync( - x => x.DepartmentId == departmentId, - cancellationToken); - - private static bool IsSameRoom(Guid roomId1, Guid roomId2) - => roomId1 == roomId2; - } -} diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index ad6b845c..24d1645b 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -16,6 +16,7 @@ public class GetAllLockersPaginated { public record Query : IRequest> { + public Guid CurrentUserId { get; init; } public string CurrentUserRole { get; init; } = null!; public Guid CurrentUserDepartmentId { get; init; } public Guid? RoomId { get; init; } @@ -46,14 +47,14 @@ public async Task> Handle(Query request, CancellationTo throw new UnauthorizedAccessException("User cannot access this resource."); } - var currentUserRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + var currentStaffRoom = await GetRoomByStaffIdAsync(request.CurrentUserId, cancellationToken); - if (currentUserRoom is null) + if (currentStaffRoom is null) { throw new UnauthorizedAccessException("User cannot access this resource."); } - if (!IsSameRoom(currentUserRoom.Id, request.RoomId.Value)) + if (!IsSameRoom(currentStaffRoom.Id, request.RoomId.Value)) { throw new UnauthorizedAccessException("User cannot access this resource."); } @@ -85,10 +86,14 @@ public async Task> Handle(Query request, CancellationTo cancellationToken); } - private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) - => await _context.Rooms.FirstOrDefaultAsync( - x => x.DepartmentId == departmentId, - cancellationToken); + private async Task GetRoomByStaffIdAsync(Guid departmentId, CancellationToken cancellationToken) + { + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == departmentId, cancellationToken); + + return staff?.Room; + } private static bool IsSameRoom(Guid roomId1, Guid roomId2) => roomId1 == roomId2; diff --git a/src/Application/Loggings/Queries/GetAllLogsPaginated.cs b/src/Application/Loggings/Queries/GetAllLogsPaginated.cs new file mode 100644 index 00000000..caced59e --- /dev/null +++ b/src/Application/Loggings/Queries/GetAllLogsPaginated.cs @@ -0,0 +1,79 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using Application.Users.Queries; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Logs.Queries; + +public class GetAllLogsPaginated +{ + public record Query : IRequest> + { + public Guid? ObjectId { get; init; } + public string? ObjectType { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.Logs.AsQueryable(); + + if (request.ObjectType is not null) + { + logs = logs.Where(x => x.ObjectType!.Equals(request.ObjectType)); + } + + if (request.ObjectId is not null) + { + logs = logs.Where(x => x.ObjectId! == request.ObjectId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => x.Message!.Contains(request.SearchTerm.Trim().ToLower())); + } + + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 10 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .OrderByDescending(x => x.Time!) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = list.Select(x => new LogDto() + { + Id = x.Id, + Event = x.Event, + Template = x.Template, + Message = x.Message, + Level = x.Level, + ObjectType = x.ObjectType, + User = _mapper.Map(_context.Users.FirstOrDefault(y => y.Id == x.UserId!)), + ObjectId = x.ObjectId!, + Time = x.Time?.ToDateTimeUtc(), + }) + .ToList(); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index 031b1131..00bf55c0 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -9,7 +10,9 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; +using Serilog.Context; namespace Application.Rooms.Commands; @@ -57,11 +60,13 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + private readonly ILogger _logger; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -95,7 +100,9 @@ public async Task Handle(Command request, CancellationToken cancellatio Created = localDateTimeNow, CreatedBy = request.CurrentUser.Id, }; - + + var result = await _context.Rooms.AddAsync(entity, cancellationToken); + var log = new RoomLog() { User = request.CurrentUser, @@ -104,7 +111,12 @@ public async Task Handle(Command request, CancellationToken cancellatio Time = localDateTimeNow, Action = RoomLogMessage.Add, }; - var result = await _context.Rooms.AddAsync(entity, cancellationToken); + + using (Logging.PushProperties(nameof(Room), entity.Id, request.CurrentUser.Id)) + { + _logger.LogAddRoom(result.Entity.Id.ToString(), department.Name); + } + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 6f56a584..d4d6a1b2 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,13 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Rooms.Commands; @@ -36,12 +39,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -68,6 +73,8 @@ public async Task Handle(Command request, CancellationToken cancellatio var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var result = _context.Rooms.Remove(room); + var log = new RoomLog() { User = request.CurrentUser, @@ -76,9 +83,15 @@ public async Task Handle(Command request, CancellationToken cancellatio Time = localDateTimeNow, Action = RoomLogMessage.Remove, }; - var result = _context.Rooms.Remove(room); await _context.RoomLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Room), room.Id, request.CurrentUser.Id)) + { + _logger.LogRemoveRoom(room.Id.ToString(), room.Department.Name); + } + return _mapper.Map(result.Entity); } } diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index d585ae1d..9c913ec7 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -9,6 +10,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Rooms.Commands; @@ -47,12 +49,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -88,6 +92,9 @@ public async Task Handle(Command request, CancellationToken cancellatio room.LastModified = localDateTimeNow; room.LastModifiedBy = request.CurrentUser.Id; + + _context.Rooms.Entry(room).State = EntityState.Modified; + var log = new RoomLog() { User = request.CurrentUser, @@ -96,9 +103,15 @@ public async Task Handle(Command request, CancellationToken cancellatio Time = localDateTimeNow, Action = RoomLogMessage.Update, }; - _context.Rooms.Entry(room).State = EntityState.Modified; + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + + using (Logging.PushProperties(nameof(Room), room.Id, request.CurrentUser.Id)) + { + _logger.LogUpdateRoom(room.Id.ToString(), room.Department.Name); + } + return _mapper.Map(room); } diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs deleted file mode 100644 index 37b9f7ff..00000000 --- a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Entities.Physical; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Queries; - -public class GetAllRoomLogsPaginated -{ - public record Query : IRequest> - { - public Guid? RoomId { get; init; } - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - } - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var logs = _context.RoomLogs - .Include(x => x.ObjectId) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .AsQueryable(); - - if (request.RoomId is not null) - { - logs = logs.Where(x => x.ObjectId! == request.RoomId); - } - - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - logs = logs.Where(x => - x.Action.ToLower().Contains(request.SearchTerm.ToLower())); - } - - return await logs - .LoggingListPaginateAsync( - request.Page, - request.Size, - _mapper.ConfigurationProvider, - cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/Application/Rooms/RoomLogExtension.cs b/src/Application/Rooms/RoomLogExtension.cs new file mode 100644 index 00000000..13d5f9fe --- /dev/null +++ b/src/Application/Rooms/RoomLogExtension.cs @@ -0,0 +1,16 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Rooms; + +public static partial class RoomLogExtension { + [LoggerMessage(Level = LogLevel.Information, Message = RoomLogMessage.Add, EventId = EventId.Add)] + public static partial void LogAddRoom(this ILogger logger, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = RoomLogMessage.Remove, EventId = EventId.Remove)] + public static partial void LogRemoveRoom(this ILogger logger, string roomId, string departmentName); + + [LoggerMessage(Level = LogLevel.Information, Message = RoomLogMessage.Update, EventId = EventId.Update)] + public static partial void LogUpdateRoom(this ILogger logger, string roomId, string departmentName); +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AssignStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs index 7627616a..1c8680b9 100644 --- a/src/Application/Staffs/Commands/AssignStaff.cs +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -8,6 +9,7 @@ using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Staffs.Commands; @@ -26,12 +28,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -92,14 +96,17 @@ public async Task Handle(Command request, CancellationToken cancellati UserId = request.CurrentUser.Id, ObjectId = staff.User.Id, Time = localDateTimeNow, - Action = UserLogMessages.Staff.AssignStaff(room.Id.ToString()), + Action = UserLogMessages.Staff.AssignStaff, }; _context.Rooms.Update(room); var result = _context.Staffs.Update(staff); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Staff), staff.Id, request.CurrentUser.Id)) + { + _logger.LogAssignStaff(staff.Id.ToString(), staff.Room.Id.ToString()); + } return _mapper.Map(result.Entity); - } } } \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index 448c2093..5e6ebfdd 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -7,6 +8,7 @@ using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Staffs.Commands; @@ -24,12 +26,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -49,6 +53,8 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("Staff is not assigned to a room."); } + var room = staff.Room; + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); staff.Room.Staff = null; @@ -66,7 +72,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = _context.Staffs.Update(staff); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - + using (Logging.PushProperties(nameof(User), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogRemoveStaffFromRoom(result.Entity.Id.ToString(), room.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Staffs/StaffLogExtensions.cs b/src/Application/Staffs/StaffLogExtensions.cs new file mode 100644 index 00000000..8577103e --- /dev/null +++ b/src/Application/Staffs/StaffLogExtensions.cs @@ -0,0 +1,13 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Staffs; + +public static partial class StaffLogExtensions { + [LoggerMessage(Level = LogLevel.Information, Message = UserLogMessages.Staff.AssignStaff, EventId = EventId.Add)] + public static partial void LogAssignStaff(this ILogger logger, string staffId, string roomId); + + [LoggerMessage(Level = LogLevel.Information, Message = UserLogMessages.Staff.RemoveFromRoom, EventId = EventId.Remove)] + public static partial void LogRemoveStaffFromRoom(this ILogger logger, string staffId, string roomId); +} \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 38c94551..a9c0fd7a 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,6 +1,9 @@ +using System.Runtime.CompilerServices; +using System.Text; using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Helpers; using Application.Identity; @@ -12,7 +15,10 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; +using Serilog.Context; +using Serilog.Core.Enrichers; namespace Application.Users.Commands; @@ -66,19 +72,21 @@ public record Command : IRequest public string? Position { get; init; } } - public class AddUserCommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly ISecurityService _securityService; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _securityService = securityService; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -125,7 +133,7 @@ public async Task Handle(Command request, CancellationToken cancellatio Created = localDateTimeNow, CreatedBy = request.CurrentUser.Id, }; - + entity.AddDomainEvent(new UserCreatedEvent(entity, password)); if (request.Role.IsStaff()) @@ -140,10 +148,14 @@ public async Task Handle(Command request, CancellationToken cancellatio UserId = request.CurrentUser.Id, ObjectId = entity.Id, Time = localDateTimeNow, - Action = UserLogMessages.Add(entity.Role), + Action = UserLogMessages.Add, }; await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(User), entity.Id, request.CurrentUser.Id)) + { + _logger.LogAddUser(result.Entity.Username, result.Entity.Email, result.Entity.Role); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 8cb22bcd..1b5f2254 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,5 +1,6 @@ using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Messages; using Application.Identity; using Application.Users.Queries; @@ -9,6 +10,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Users.Commands; @@ -46,12 +48,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -92,6 +96,10 @@ public async Task Handle(Command request, CancellationToken cancellatio var result = _context.Users.Update(user); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(User), result.Entity.Id, request.CurrentUser.Id)) + { + _logger.LogUpdateUser(result.Entity.Username); + } return _mapper.Map(result.Entity); } diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs deleted file mode 100644 index ee1c3e7c..00000000 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities; -using Domain.Entities.Logging; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Users.Queries; - -public class GetAllUserLogsPaginated -{ - public record Query : IRequest> - { - public Guid? UserId { get; init; } - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - } - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var logs = _context.UserLogs - .Include(x => x.ObjectId) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .AsQueryable(); - - if (request.UserId is not null) - { - logs = logs.Where(x => x.ObjectId! == request.UserId); - } - - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - logs = logs.Where(x => - x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); - } - - return await logs - .LoggingListPaginateAsync( - request.Page, - request.Size, - _mapper.ConfigurationProvider, - cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/Application/Users/UserLogExtensions.cs b/src/Application/Users/UserLogExtensions.cs new file mode 100644 index 00000000..7607bc02 --- /dev/null +++ b/src/Application/Users/UserLogExtensions.cs @@ -0,0 +1,13 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; + +namespace Application.Users; + +public static partial class UserLogExtensions +{ + [LoggerMessage(Level = LogLevel.Information, Message = UserLogMessages.Add)] + public static partial void LogAddUser(this ILogger logger, string username, string email, string role); + + [LoggerMessage(Level = LogLevel.Information, Message = UserLogMessages.Update)] + public static partial void LogUpdateUser(this ILogger logger, string username); +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index c842828d..3a1f60ff 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,5 +1,6 @@ using System.Reflection; using Application.Common.Interfaces; +using Application.Common.Models; using Domain.Entities; using Domain.Entities.Digital; using Domain.Entities.Logging; @@ -29,7 +30,7 @@ public ApplicationDbContext( public DbSet Documents => Set(); public DbSet ImportRequests => Set(); public DbSet Borrows => Set(); - public DbSet Permissions => Set(); + public DbSet Permissions => Set(); public DbSet UserGroups => Set(); public DbSet Files => Set(); @@ -38,6 +39,8 @@ public ApplicationDbContext( public DbSet RefreshTokens => Set(); public DbSet ResetPasswordTokens => Set(); + public DbSet Logs => Set(); + public DbSet RoomLogs => Set(); public DbSet LockerLogs => Set(); public DbSet FolderLogs => Set(); diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index f224fd2b..6725f57e 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using NodaTime; using Serilog; +using Serilog.Context; namespace Infrastructure.Persistence; @@ -18,7 +19,7 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); try { - await TrySeedAsync(context, securitySettings!.Pepper); + await TrySeedAsync(context, securitySettings!.Pepper, logger); } catch (Exception ex) { @@ -27,7 +28,7 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi } } - private static async Task TrySeedAsync(ApplicationDbContext context, string pepper) + private static async Task TrySeedAsync(ApplicationDbContext context, string pepper, ILogger logger) { var department = new Department() { diff --git a/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.Designer.cs new file mode 100644 index 00000000..9929cf86 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.Designer.cs @@ -0,0 +1,1105 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230624100226_TrueLogging")] + partial class TrueLogging + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.cs b/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.cs new file mode 100644 index 00000000..0dd3f20e --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230624100226_TrueLogging.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class TrueLogging : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Logs", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Template = table.Column(type: "text", nullable: true), + Message = table.Column(type: "text", nullable: true), + Level = table.Column(type: "text", nullable: true), + Time = table.Column(type: "timestamp with time zone", nullable: true), + Event = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: true), + ObjectId = table.Column(type: "uuid", nullable: true), + ObjectType = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Logs", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Logs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index ba5d9a63..7c7ab31e 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -23,6 +23,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + modelBuilder.Entity("Domain.Entities.Department", b => { b.Property("Id") From bcd92463167cc3e0a887d20d02158e66f51f9365 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:14:54 +0700 Subject: [PATCH 076/162] delete: everything of user group (#308) --- .../Interfaces/IApplicationDbContext.cs | 1 - .../Models/Dtos/Digital/UserGroupDto.cs | 9 - src/Application/Common/Models/Dtos/UserDto.cs | 2 - .../UserGroups/Commands/CreateUserGroup.cs | 61 ------ .../UserGroups/Commands/DeleteUserGroup.cs | 41 ---- .../UserGroups/Commands/UpdateUserGroup.cs | 63 ------- .../Queries/GetAllUserGroupsPaginated.cs | 62 ------ .../UserGroups/Queries/GetUserGroupById.cs | 42 ----- src/Domain/Entities/Digital/UserGroup.cs | 9 - src/Domain/Entities/User.cs | 2 - .../Persistence/ApplicationDbContext.cs | 1 - .../Configurations/UserGroupConfiguration.cs | 25 --- .../BaseClassFixture.cs | 10 - .../Commands/CreateUserGroupTests.cs | 58 ------ .../Commands/DeleteUserGroupTests.cs | 51 ----- .../Commands/UpdateUserGroupTests.cs | 87 --------- .../Queries/GetAllUserGroupsPaginatedTests.cs | 176 ------------------ .../Queries/GetUserGroupByIdTests.cs | 53 ------ .../Common/Mappings/MappingTests.cs | 1 - 19 files changed, 754 deletions(-) delete mode 100644 src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs delete mode 100644 src/Application/UserGroups/Commands/CreateUserGroup.cs delete mode 100644 src/Application/UserGroups/Commands/DeleteUserGroup.cs delete mode 100644 src/Application/UserGroups/Commands/UpdateUserGroup.cs delete mode 100644 src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs delete mode 100644 src/Application/UserGroups/Queries/GetUserGroupById.cs delete mode 100644 src/Domain/Entities/Digital/UserGroup.cs delete mode 100644 src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs delete mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs delete mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs delete mode 100644 tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs delete mode 100644 tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs delete mode 100644 tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index fa550c14..2b0dc101 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -21,7 +21,6 @@ public interface IApplicationDbContext public DbSet Borrows { get; } public DbSet Permissions { get; } - public DbSet UserGroups { get; } public DbSet Files { get; } public DbSet Entries { get; } diff --git a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs deleted file mode 100644 index 3707c15c..00000000 --- a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Application.Common.Mappings; -using Domain.Entities.Digital; - -namespace Application.Common.Models.Dtos.Digital; - -public class UserGroupDto : BaseDto, IMapFrom -{ - public string Name { get; set; } = null!; -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index 737b6e77..7d5a7c2e 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -22,8 +22,6 @@ public class UserDto : BaseDto, IMapFrom public DateTime? LastModified { get; set; } public Guid? LastModifiedBy { get; set; } - public IEnumerable UserGroups { get; set; } - public void Mapping(Profile profile) { profile.CreateMap() diff --git a/src/Application/UserGroups/Commands/CreateUserGroup.cs b/src/Application/UserGroups/Commands/CreateUserGroup.cs deleted file mode 100644 index 467cc6c4..00000000 --- a/src/Application/UserGroups/Commands/CreateUserGroup.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Digital; -using AutoMapper; -using Domain.Entities.Digital; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.UserGroups.Commands; - -public class CreateUserGroup -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.Name) - .NotEmpty().WithMessage("Name is required.") - .MaximumLength(32).WithMessage("Name cannot exceed 32 characters."); - } - } - - public record Command : IRequest - { - public string Name { get; init; } = null!; - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var userGroup = await _context.UserGroups.FirstOrDefaultAsync(x => x.Name.ToLower().Trim().Equals(request.Name.ToLower().Trim()), cancellationToken); - - if (userGroup is not null) - { - throw new ConflictException("User group name already exists."); - } - - var entity = new UserGroup() - { - Name = request.Name.Trim(), - }; - - var result = await _context.UserGroups.AddAsync(entity, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} diff --git a/src/Application/UserGroups/Commands/DeleteUserGroup.cs b/src/Application/UserGroups/Commands/DeleteUserGroup.cs deleted file mode 100644 index 7e601dd8..00000000 --- a/src/Application/UserGroups/Commands/DeleteUserGroup.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Digital; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.UserGroups.Commands; - -public class DeleteUserGroup -{ - public record Command : IRequest - { - public Guid UserGroupId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var userGroup = await _context.UserGroups.FirstOrDefaultAsync(x => x.Id == request.UserGroupId, cancellationToken); - - if (userGroup is null) - { - throw new KeyNotFoundException("User group does not exist."); - } - - var result = _context.UserGroups.Remove(userGroup); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} diff --git a/src/Application/UserGroups/Commands/UpdateUserGroup.cs b/src/Application/UserGroups/Commands/UpdateUserGroup.cs deleted file mode 100644 index fb6449e7..00000000 --- a/src/Application/UserGroups/Commands/UpdateUserGroup.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Digital; -using AutoMapper; -using Domain.Entities.Digital; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.UserGroups.Commands; - -public class UpdateUserGroup -{ - public record Command : IRequest - { - public Guid UserGroupId { get; init; } - public string Name { get; init; } = null!; - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var userGroup = await _context.UserGroups - .FirstOrDefaultAsync(x => x.Id == request.UserGroupId, cancellationToken); - - if (userGroup is null) - { - throw new KeyNotFoundException("User group does not exist."); - } - - var existedUserGroup = await _context.UserGroups - .FirstOrDefaultAsync(x => x.Name.ToLower().Trim().Equals(request.Name.ToLower().Trim()) - && x.Id != userGroup.Id, cancellationToken); - - if (existedUserGroup is not null) - { - throw new ConflictException("New user group name already exists."); - } - - var updatedUserGroup = new UserGroup() - { - Id = userGroup.Id, - Name = request.Name.Trim(), - Users = userGroup.Users, - }; - - _context.UserGroups.Entry(userGroup).State = EntityState.Detached; - _context.UserGroups.Entry(updatedUserGroup).State = EntityState.Modified; - - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(updatedUserGroup); - } - } -} diff --git a/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs b/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs deleted file mode 100644 index d06988c2..00000000 --- a/src/Application/UserGroups/Queries/GetAllUserGroupsPaginated.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Digital; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.UserGroups.Queries; - -public class GetAllUserGroupsPaginated -{ - public record Query : IRequest> - { - public string? SearchTerm { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } - }; - - public class QueryHandler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var userGroups = _context.UserGroups.AsQueryable(); - if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) - { - userGroups = userGroups.Where(x => - x.Name.ToLower().Trim().Contains(request.SearchTerm.ToLower().Trim())); - } - - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(UserGroupDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await userGroups.CountAsync(cancellationToken); - var list = await userGroups - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); - } - } -} \ No newline at end of file diff --git a/src/Application/UserGroups/Queries/GetUserGroupById.cs b/src/Application/UserGroups/Queries/GetUserGroupById.cs deleted file mode 100644 index 412cf807..00000000 --- a/src/Application/UserGroups/Queries/GetUserGroupById.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Digital; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.UserGroups.Queries; - -public class GetUserGroupById -{ - public record Query : IRequest - { - public Guid UserGroupId { get; set; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var userGroup = await _context.UserGroups - .FirstOrDefaultAsync( - x => x.Id == request.UserGroupId - , cancellationToken); - - if (userGroup is null) - { - throw new KeyNotFoundException("User group does not exist."); - } - - return _mapper.Map(userGroup); - } - } -} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/UserGroup.cs b/src/Domain/Entities/Digital/UserGroup.cs deleted file mode 100644 index 23355d1e..00000000 --- a/src/Domain/Entities/Digital/UserGroup.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Domain.Common; - -namespace Domain.Entities.Digital; - -public class UserGroup : BaseEntity -{ - public string Name { get; set; } = null!; - public ICollection Users { get; set; } = new List(); -} \ No newline at end of file diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index 014d974e..c9bb7392 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -17,6 +17,4 @@ public class User : BaseAuditableEntity public string? Position { get; set; } public bool IsActive { get; set; } public bool IsActivated { get; set; } - - public ICollection UserGroups { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 3a1f60ff..7bd253de 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -32,7 +32,6 @@ public ApplicationDbContext( public DbSet Borrows => Set(); public DbSet Permissions => Set(); - public DbSet UserGroups => Set(); public DbSet Files => Set(); public DbSet Entries => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs deleted file mode 100644 index bd18e3ac..00000000 --- a/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Domain.Entities; -using Domain.Entities.Digital; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Infrastructure.Persistence.Configurations; - -public class UserGroupConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(x => x.Id); - builder.Property(x => x.Id) - .ValueGeneratedOnAdd(); - - builder.HasMany(x => x.Users) - .WithMany(x => x.UserGroups).UsingEntity( - "Memberships", - l => l.HasOne(typeof(User)).WithMany().HasForeignKey("UserId").HasPrincipalKey(nameof(User.Id)), - r => r.HasOne(typeof(UserGroup)).WithMany().HasForeignKey("UserGroupId").HasPrincipalKey(nameof(UserGroup.Id)), - j => j.HasKey("UserId", "UserGroupId")); - - builder.HasAlternateKey(x => x.Name); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index f4644e5e..d724d1e5 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -203,16 +203,6 @@ protected static Staff CreateStaff(User user, Room? room) }; } - protected static UserGroup CreateUserGroup(User[] users) - { - return new UserGroup() - { - Id = Guid.NewGuid(), - Name = new Faker().Commerce.ProductName(), - Users = users, - }; - } - protected static FileEntity CreateFile() { return new FileEntity() diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs deleted file mode 100644 index ac75ecd0..00000000 --- a/tests/Application.Tests.Integration/UserGroups/Commands/CreateUserGroupTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Application.Common.Exceptions; -using Application.UserGroups.Commands; -using Bogus; -using Domain.Entities; -using Domain.Entities.Digital; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.UserGroups.Commands; - -public class CreateUserGroupTests : BaseClassFixture -{ - public CreateUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) - { - - } - - [Fact] - public async Task ShouldCreateUserGroup_WhenCreateDetailsAreValid() - { - // Arrange - var command = new CreateUserGroup.Command() - { - Name = new Faker().Commerce.Department(), - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Name.Should().Be(command.Name); - - // Cleanup - Remove(await FindAsync(result.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenUserGroupNameExists() - { - // Arrange - var userGroup = CreateUserGroup(new User[] {}); - await AddAsync(userGroup); - - var command = new CreateUserGroup.Command() - { - Name = userGroup.Name, - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync().WithMessage("User group name already exists."); - - // Cleanup - Remove(userGroup); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs deleted file mode 100644 index 12db7afc..00000000 --- a/tests/Application.Tests.Integration/UserGroups/Commands/DeleteUserGroupTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Application.UserGroups.Commands; -using Domain.Entities; -using Domain.Entities.Digital; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.UserGroups.Commands; - -public class DeleteUserGroupTests : BaseClassFixture -{ - public DeleteUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) - { - - } - - [Fact] - public async Task ShouldDeleteUserGroup_WhenUserGroupExists() - { - // Arrange - var userGroup = CreateUserGroup(Array.Empty()); - await AddAsync(userGroup); - - var command = new DeleteUserGroup.Command() - { - UserGroupId = userGroup.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - var entityCheck = await FindAsync(result.Id); - entityCheck.Should().BeNull(); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() - { - // Arrange - var command = new DeleteUserGroup.Command() - { - UserGroupId = Guid.NewGuid(), - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync().WithMessage("User group does not exist."); - } -} diff --git a/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs b/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs deleted file mode 100644 index c7177006..00000000 --- a/tests/Application.Tests.Integration/UserGroups/Commands/UpdateUserGroupTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Application.Common.Exceptions; -using Application.UserGroups.Commands; -using Domain.Entities; -using Domain.Entities.Digital; -using FluentAssertions; -using Infrastructure.Persistence; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Application.Tests.Integration.UserGroups.Commands; - -public class UpdateUserGroupTests : BaseClassFixture -{ - public UpdateUserGroupTests(CustomApiFactory apiFactory) : base(apiFactory) - { - - } - - [Fact] - public async Task ShouldUpdateLocker_WhenDetailsAreValid() - { - // Arrange - var userGroup = CreateUserGroup(Array.Empty()); - await AddAsync(userGroup); - - var command = new UpdateUserGroup.Command() - { - UserGroupId = userGroup.Id, - Name = "Updated name", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Name.Should().Be(command.Name); - - // Cleanup - Remove(await FindAsync(userGroup.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() - { - // Arrange - var command = new UpdateUserGroup.Command() - { - UserGroupId = Guid.NewGuid(), - Name = "Name", - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("User group does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenNewUserGroupNameAlreadyExists() - { - // Arrange - var userGroup = CreateUserGroup(Array.Empty()); - await AddAsync(userGroup); - - var updateUserGroup = CreateUserGroup(Array.Empty()); - await AddAsync(updateUserGroup); - - var command = new UpdateUserGroup.Command() - { - UserGroupId = updateUserGroup.Id, - Name = userGroup.Name, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("New user group name already exists."); - - // Cleanup - Remove(userGroup); - Remove(updateUserGroup); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs b/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs deleted file mode 100644 index d9608e01..00000000 --- a/tests/Application.Tests.Integration/UserGroups/Queries/GetAllUserGroupsPaginatedTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Digital; -using Application.UserGroups.Queries; -using AutoMapper; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.UserGroups.Queries; - -public class GetAllUserGroupsPaginatedTests : BaseClassFixture -{ - private readonly IMapper _mapper; - public GetAllUserGroupsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(x => x.AddProfile()); - _mapper = configuration.CreateMapper(); - } - - [Fact] - public async Task ShouldReturnUserGroups_WhenUserGroupsExist() - { - // Arrange - var userGroup1 = CreateUserGroup(Array.Empty()); - var userGroup2 = CreateUserGroup(Array.Empty()); - - await AddAsync(userGroup1); - await AddAsync(userGroup2); - var query = new GetAllUserGroupsPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new [] {userGroup1, userGroup2})); - - // Cleanup - Remove(userGroup1); - Remove(userGroup2); - } - - [Fact] - public async Task ShouldReturnEmptyList_WhenNoUserGroupsExist() - { - // Arrange - var query = new GetAllUserGroupsPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(0); - } - - [Fact] - public async Task ShouldReturnAscendingOrderById_WhenWrongSortByIsProvided() - { - // Arrange - var userGroup1 = CreateUserGroup(Array.Empty()); - var userGroup2 = CreateUserGroup(Array.Empty()); - - await AddAsync(userGroup1); - await AddAsync(userGroup2); - var query = new GetAllUserGroupsPaginated.Query() - { - SortBy = "example" - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) - .OrderBy(x => x.Id)); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(userGroup1); - Remove(userGroup2); - } - - [Fact] - public async Task ShouldReturnAscendingOrderByName_WhenSearchTermIsProvided() - { - // Arrange - var userGroup1 = CreateUserGroup(Array.Empty()); - var userGroup2 = CreateUserGroup(Array.Empty()); - var userGroup3 = CreateUserGroup(Array.Empty()); - userGroup3.Name = userGroup1.Name + "2"; - - await AddAsync(userGroup1); - await AddAsync(userGroup2); - await AddAsync(userGroup3); - var query = new GetAllUserGroupsPaginated.Query() - { - SearchTerm = userGroup1.Name - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup3 }) - .OrderBy(x => x.Id)); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(userGroup1); - Remove(userGroup2); - Remove(userGroup3); - } - - [Fact] - public async Task ShouldReturnSortByInAscendingOrder_WhenCorrectSortByIsProvided() - { - // Arrange - var userGroup1 = CreateUserGroup(Array.Empty()); - var userGroup2 = CreateUserGroup(Array.Empty()); - - await AddAsync(userGroup1); - await AddAsync(userGroup2); - var query = new GetAllUserGroupsPaginated.Query() - { - SortBy = "Name" - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) - .OrderBy(x => x.Name)); - result.Items.Should().BeInAscendingOrder(x => x.Name); - - // Cleanup - Remove(userGroup1); - Remove(userGroup2); - } - - [Fact] - public async Task ShouldReturnInDescendingOrderOfId_WhenDescendingSortOrderIsProvided() - { - // Arrange - var userGroup1 = CreateUserGroup(Array.Empty()); - var userGroup2 = CreateUserGroup(Array.Empty()); - - await AddAsync(userGroup1); - await AddAsync(userGroup2); - var query = new GetAllUserGroupsPaginated.Query() - { - SortOrder = "desc" - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new[] { userGroup1, userGroup2 }) - .OrderByDescending(x => x.Id)); - result.Items.Should().BeInDescendingOrder(x => x.Id); - - // Cleanup - Remove(userGroup1); - Remove(userGroup2); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs b/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs deleted file mode 100644 index 9053d204..00000000 --- a/tests/Application.Tests.Integration/UserGroups/Queries/GetUserGroupByIdTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Application.UserGroups.Queries; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.UserGroups.Queries; - -public class GetUserGroupByIdTests : BaseClassFixture -{ - public GetUserGroupByIdTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldReturnUserGroup_WhenThatUserGroupExists() - { - // Arrange - var userGroup = CreateUserGroup(Array.Empty()); - - await AddAsync(userGroup); - - var query = new GetUserGroupById.Query() - { - UserGroupId = userGroup.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Name.Should().Be(userGroup.Name); - - // Cleanup - Remove(userGroup); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenUserGroupDoesNotExist() - { - // Arrange - var query = new GetUserGroupById.Query() - { - UserGroupId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("User group does not exist."); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 7f6a4ac8..2d635750 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -48,7 +48,6 @@ public void ShouldHaveValidConfiguration() [InlineData(typeof(RefreshToken), typeof(RefreshTokenDto))] [InlineData(typeof(FileEntity), typeof(FileDto))] [InlineData(typeof(Entry), typeof(EntryDto))] - [InlineData(typeof(UserGroup), typeof(UserGroupDto))] [InlineData(typeof(User), typeof(IssuerDto))] [InlineData(typeof(Document), typeof(IssuedDocumentDto))] [InlineData(typeof(ImportRequest), typeof(ImportRequestDto))] From 888647861737670cb5517d17fb0e874f50ab7ace Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sat, 1 Jul 2023 12:57:36 +0700 Subject: [PATCH 077/162] implement uploading digital files (#297) * feat: implement upload digital file * update regex * refactoring * Add 500 status code error * update a couple things: - fix some tweaks in uploading files - we'll see later * I forgot to ship this * hehe * update: upload so that an endpoint can both upload a file and create a directory * fix: bad request messages --------- Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> Co-authored-by: Nguyen Quang Chien --- src/Api/Controllers/EntriesController.cs | 87 ++ .../DigitalFile/UploadDigitalFileRequest.cs | 12 + .../Common/Models/Dtos/Digital/EntryDto.cs | 27 +- .../Common/Models/Dtos/Digital/FileDto.cs | 2 + .../Digital/Commands/UploadDigitalFile.cs | 119 ++ src/Domain/Entities/Digital/Entry.cs | 9 +- src/Domain/Entities/Digital/FileEntity.cs | 1 + .../Configurations/EntryConfiguration.cs | 10 + .../Configurations/FileConfiguration.cs | 3 + .../20230629012929_FileAndEntry.Designer.cs | 1107 +++++++++++++++++ .../Migrations/20230629012929_FileAndEntry.cs | 125 ++ .../ApplicationDbContextModelSnapshot.cs | 39 + 12 files changed, 1538 insertions(+), 3 deletions(-) create mode 100644 src/Api/Controllers/EntriesController.cs create mode 100644 src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs create mode 100644 src/Application/Digital/Commands/UploadDigitalFile.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs new file mode 100644 index 00000000..6f5dad46 --- /dev/null +++ b/src/Api/Controllers/EntriesController.cs @@ -0,0 +1,87 @@ +using Api.Controllers.Payload.Requests.DigitalFile; +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using Application.Digital.Commands; +using Application.Identity; +using FluentValidation.Results; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class EntriesController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + + public EntriesController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpPost] + public async Task>> UploadDigitalFile( + [FromForm] UploadDigitalFileRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + + if (request is { IsDirectory: true, File: not null }) + { + throw new RequestValidationException(new List() + { + new("File", "Cannot create a directory with a file.") + }); + } + + if (request is { IsDirectory: false, File: null }) + { + throw new RequestValidationException(new List() + { + new("File", "Cannot upload with no files.") + }); + } + + UploadDigitalFile.Command command; + + if (request.IsDirectory) + { + command = new UploadDigitalFile.Command() + { + CurrentUser = currentUser, + Path = request.Path, + Name = request.Name, + IsDirectory = true, + FileType = null, + FileData = null, + FileExtension = null, + }; + } + else + { + var fileData = new MemoryStream(); + await request.File!.CopyToAsync(fileData); + var lastDotIndex = request.File.FileName.LastIndexOf(".", StringComparison.Ordinal); + var extension = + request.File.FileName.Substring(lastDotIndex + 1, request.File.FileName.Length - lastDotIndex - 1); + command = new UploadDigitalFile.Command() + { + CurrentUser = currentUser, + Path = request.Path, + Name = request.Name, + IsDirectory = false, + FileType = request.File.ContentType, + FileData = fileData, + FileExtension = extension, + }; + } + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs b/src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs new file mode 100644 index 00000000..08f04215 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs @@ -0,0 +1,12 @@ +namespace Api.Controllers.Payload.Requests.DigitalFile; + +/// +/// +/// +public class UploadDigitalFileRequest +{ + public string Name { get; set; } = null!; + public string Path { get; set; } = null!; + public bool IsDirectory { get; set; } + public IFormFile? File { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs index b3882a91..7246ece6 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -1,4 +1,6 @@ using Application.Common.Mappings; +using Application.Users.Queries; +using AutoMapper; using Domain.Entities.Digital; namespace Application.Common.Models.Dtos.Digital; @@ -7,5 +9,28 @@ public class EntryDto : BaseDto, IMapFrom { public string Name { get; set; } = null!; public string Path { get; set; } = null!; - public FileDto? File { get; set; } + public Guid? FileId { get; set; } + public string? FileType { get; set; } + public string? FileExtension { get; set; } + public bool IsDirectory { get; set; } + + public UserDto Owner { get; set; } = null!; + public DateTime Created { get; set; } + public Guid? CreatedBy { get; set; } + public DateTime? LastModified { get; set; } + public Guid? LastModifiedBy { get; set; } + public UserDto Uploader { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Created, + opt => opt.MapFrom(src => src.Created.ToDateTimeUnspecified())) + .ForMember(dest => dest.LastModified, + opt => opt.MapFrom(src => src.LastModified.Value.ToDateTimeUnspecified())) + .ForMember(dest => dest.FileType, + opt => opt.MapFrom(src => src.File.FileType)) + .ForMember(dest => dest.FileExtension, + opt => opt.MapFrom(src => src.File.FileExtension)); + } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/FileDto.cs b/src/Application/Common/Models/Dtos/Digital/FileDto.cs index c23b2383..60c6c796 100644 --- a/src/Application/Common/Models/Dtos/Digital/FileDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/FileDto.cs @@ -6,4 +6,6 @@ namespace Application.Common.Models.Dtos.Digital; public class FileDto : BaseDto, IMapFrom { public string FileType { get; set; } = null!; + public string? FileExtension { get; set; } + public string[] FileData { get; set; } = null!; } \ No newline at end of file diff --git a/src/Application/Digital/Commands/UploadDigitalFile.cs b/src/Application/Digital/Commands/UploadDigitalFile.cs new file mode 100644 index 00000000..13e87bd8 --- /dev/null +++ b/src/Application/Digital/Commands/UploadDigitalFile.cs @@ -0,0 +1,119 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class UploadDigitalFile { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); + + RuleFor(x => x.Path) + .NotEmpty().WithMessage("File's path is required.") + .Matches("^(/(?!/)[a-z_.\\-0-9]*)+(? + { + public User CurrentUser { get; init; } = null!; + public string Name { get; init; } = null!; + public string Path { get; init; } = null!; + public bool IsDirectory { get; init; } + public MemoryStream? FileData { get; init; } + public string? FileType { get; init; } + public string? FileExtension { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var baseDirectoryExists = await _context.Entries.AnyAsync( + x => request.Path.Trim().ToLower().Equals((x.Path + x.Name).ToLower()) + && x.FileId == null, cancellationToken); + + if (!request.Path.Equals("/") && !baseDirectoryExists) + { + throw new ConflictException("Base directory does not exist."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var entryEntity = new Entry() + { + Name = request.Name.Trim(), + Path = request.Path.Trim(), + CreatedBy = request.CurrentUser.Id, + Uploader = request.CurrentUser, + Created = localDateTimeNow, + Owner = request.CurrentUser, + OwnerId = request.CurrentUser.Id, + }; + + if (request.IsDirectory) + { + var entry = await _context.Entries.FirstOrDefaultAsync( + x => x.Name.Trim().Equals(request.Name.Trim()) + && x.Path.Trim().Equals(request.Path.Trim()) + && x.FileId == null, cancellationToken); + + if (entry is not null) + { + throw new ConflictException("Directory already exists."); + } + } + else + { + // Make this dynamic + if (request.FileData!.Length > 20971520) + { + throw new ConflictException("File size must be lower than 20MB"); + } + + var lastDotIndex = request.Name.LastIndexOf(".", StringComparison.Ordinal); + var fileExtension = request.Name.Substring(lastDotIndex + 1, request.Name.Length - lastDotIndex - 1); + + var fileEntity = new FileEntity() + { + FileData = request.FileData.ToArray(), + FileType = request.FileType!, + FileExtension = request.FileExtension, + }; + entryEntity.FileId = fileEntity.Id; + entryEntity.File = fileEntity; + + await _context.Files.AddAsync(fileEntity, cancellationToken); + } + + var result = await _context.Entries.AddAsync(entryEntity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs index 712bc0c5..7aa32206 100644 --- a/src/Domain/Entities/Digital/Entry.cs +++ b/src/Domain/Entities/Digital/Entry.cs @@ -1,12 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; using Domain.Common; namespace Domain.Entities.Digital; -public class Entry : BaseEntity +public class Entry : BaseAuditableEntity { public string Name { get; set; } = null!; public string Path { get; set; } = null!; public Guid? FileId { get; set; } - + public Guid OwnerId { get; set; } + [NotMapped] public bool IsDirectory => FileId is null; + public virtual FileEntity? File { get; set; } + public virtual User Uploader { get; set; } = null!; + public virtual User Owner { get; set; } = null!; } \ No newline at end of file diff --git a/src/Domain/Entities/Digital/FileEntity.cs b/src/Domain/Entities/Digital/FileEntity.cs index e965dbc7..70c50363 100644 --- a/src/Domain/Entities/Digital/FileEntity.cs +++ b/src/Domain/Entities/Digital/FileEntity.cs @@ -6,4 +6,5 @@ public class FileEntity : BaseEntity { public string FileType { get; set; } = null!; public byte[] FileData { get; set; } = null!; + public string? FileExtension { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs index 433d872e..01269855 100644 --- a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs @@ -23,5 +23,15 @@ public void Configure(EntityTypeBuilder builder) .WithOne() .HasForeignKey(x => x.FileId) .IsRequired(false); + + builder.HasOne(x => x.Uploader) + .WithMany() + .HasForeignKey(x => x.CreatedBy) + .IsRequired(); + + builder.HasOne(x => x.Owner) + .WithMany() + .HasForeignKey(x => x.OwnerId) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs index d1f34b90..b0b9dae5 100644 --- a/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs @@ -18,5 +18,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.FileData) .IsRequired(); + + builder.Property(x => x.FileExtension) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.Designer.cs new file mode 100644 index 00000000..1ace2e14 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.Designer.cs @@ -0,0 +1,1107 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230629012929_FileAndEntry")] + partial class FileAndEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.cs b/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.cs new file mode 100644 index 00000000..5be77408 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230629012929_FileAndEntry.cs @@ -0,0 +1,125 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class FileAndEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FileExtension", + table: "Files", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Entries", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Entries", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Entries", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "OwnerId", + table: "Entries", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_Entries_CreatedBy", + table: "Entries", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Entries_OwnerId", + table: "Entries", + column: "OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_Entries_Users_CreatedBy", + table: "Entries", + column: "CreatedBy", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Entries_Users_OwnerId", + table: "Entries", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Entries_Users_CreatedBy", + table: "Entries"); + + migrationBuilder.DropForeignKey( + name: "FK_Entries_Users_OwnerId", + table: "Entries"); + + migrationBuilder.DropIndex( + name: "IX_Entries_CreatedBy", + table: "Entries"); + + migrationBuilder.DropIndex( + name: "IX_Entries_OwnerId", + table: "Entries"); + + migrationBuilder.DropColumn( + name: "FileExtension", + table: "Files"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Entries"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Entries"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Entries"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Entries"); + + migrationBuilder.DropColumn( + name: "OwnerId", + table: "Entries"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 7c7ab31e..c7592d1d 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -84,23 +84,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + b.Property("FileId") .HasColumnType("uuid"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("OwnerId") + .HasColumnType("uuid"); + b.Property("Path") .IsRequired() .HasColumnType("text"); b.HasKey("Id"); + b.HasIndex("CreatedBy"); + b.HasIndex("FileId") .IsUnique(); + b.HasIndex("OwnerId"); + b.ToTable("Entries"); }); @@ -114,6 +134,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("bytea"); + b.Property("FileExtension") + .HasColumnType("text"); + b.Property("FileType") .IsRequired() .HasMaxLength(256) @@ -804,11 +827,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Digital.Entry", b => { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.HasOne("Domain.Entities.Digital.FileEntity", "File") .WithOne() .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); }); modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => From 5fe7e6af0c5b8ae996cdc52eb4da7c6facfaebab Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 1 Jul 2023 14:09:18 +0700 Subject: [PATCH 078/162] update: document entity now has a file entity instead of entry --- src/Domain/Entities/Physical/Document.cs | 4 +- .../Configurations/DocumentConfiguration.cs | 4 +- ..._DocumentHasFileInsteadOfEntry.Designer.cs | 1097 +++++++++++++++++ ...701070615_DocumentHasFileInsteadOfEntry.cs | 109 ++ .../ApplicationDbContextModelSnapshot.cs | 57 +- 5 files changed, 1215 insertions(+), 56 deletions(-) create mode 100644 src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.cs diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index 0de04af1..b5a491ea 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -13,9 +13,9 @@ public class Document : BaseAuditableEntity public Department? Department { get; set; } public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } - public Guid? EntryId { get; set; } + public Guid? FileId { get; set; } public bool IsPrivate { get; set; } public User? Importer { get; set; } - public virtual Entry? Entry { get; set; } + public virtual FileEntity? File { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 3bb00c66..59aa2c77 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -42,9 +42,9 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Status) .IsRequired(); - builder.HasOne(x => x.Entry) + builder.HasOne(x => x.File) .WithOne() - .HasForeignKey(x => x.EntryId) + .HasForeignKey(x => x.FileId) .IsRequired(false); builder.Property(x => x.IsPrivate) diff --git a/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.Designer.cs new file mode 100644 index 00000000..d39293a4 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.Designer.cs @@ -0,0 +1,1097 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230701070615_DocumentHasFileInsteadOfEntry")] + partial class DocumentHasFileInsteadOfEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.cs b/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.cs new file mode 100644 index 00000000..89ab2d7f --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701070615_DocumentHasFileInsteadOfEntry.cs @@ -0,0 +1,109 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DocumentHasFileInsteadOfEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents"); + + migrationBuilder.DropTable( + name: "Memberships"); + + migrationBuilder.DropTable( + name: "UserGroups"); + + migrationBuilder.RenameColumn( + name: "EntryId", + table: "Documents", + newName: "FileId"); + + migrationBuilder.RenameIndex( + name: "IX_Documents_EntryId", + table: "Documents", + newName: "IX_Documents_FileId"); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Files_FileId", + table: "Documents", + column: "FileId", + principalTable: "Files", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Files_FileId", + table: "Documents"); + + migrationBuilder.RenameColumn( + name: "FileId", + table: "Documents", + newName: "EntryId"); + + migrationBuilder.RenameIndex( + name: "IX_Documents_FileId", + table: "Documents", + newName: "IX_Documents_EntryId"); + + migrationBuilder.CreateTable( + name: "UserGroups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserGroups", x => x.Id); + table.UniqueConstraint("AK_UserGroups_Name", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "Memberships", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + UserGroupId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Memberships", x => new { x.UserId, x.UserGroupId }); + table.ForeignKey( + name: "FK_Memberships_UserGroups_UserGroupId", + column: x => x.UserGroupId, + principalTable: "UserGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Memberships_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Memberships_UserGroupId", + table: "Memberships", + column: "UserGroupId"); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents", + column: "EntryId", + principalTable: "Entries", + principalColumn: "Id"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index c7592d1d..2a64bcf0 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -147,23 +147,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Files"); }); - modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasAlternateKey("Name"); - - b.ToTable("UserGroups"); - }); - modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => { b.Property("Id") @@ -415,7 +398,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("EntryId") + b.Property("FileId") .HasColumnType("uuid"); b.Property("FolderId") @@ -445,7 +428,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("DepartmentId"); - b.HasIndex("EntryId") + b.HasIndex("FileId") .IsUnique(); b.HasIndex("FolderId"); @@ -810,21 +793,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); - modelBuilder.Entity("Memberships", b => - { - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("UserGroupId") - .HasColumnType("uuid"); - - b.HasKey("UserId", "UserGroupId"); - - b.HasIndex("UserGroupId"); - - b.ToTable("Memberships"); - }); - modelBuilder.Entity("Domain.Entities.Digital.Entry", b => { b.HasOne("Domain.Entities.User", "Uploader") @@ -959,9 +927,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("DepartmentId"); - b.HasOne("Domain.Entities.Digital.Entry", "Entry") + b.HasOne("Domain.Entities.Digital.FileEntity", "File") .WithOne() - .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); b.HasOne("Domain.Entities.Physical.Folder", "Folder") .WithMany("Documents") @@ -973,7 +941,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); - b.Navigation("Entry"); + b.Navigation("File"); b.Navigation("Folder"); @@ -1099,21 +1067,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); }); - modelBuilder.Entity("Memberships", b => - { - b.HasOne("Domain.Entities.Digital.UserGroup", null) - .WithMany() - .HasForeignKey("UserGroupId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Domain.Entities.User", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Domain.Entities.Department", b => { b.Navigation("Rooms"); From 960a7348a27eff5633617b24d5dc40b4bf2baa9f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 1 Jul 2023 16:19:39 +0700 Subject: [PATCH 079/162] add: download document endpoint - the constraint is only the document importer is actually the user downloading, this might be extended later - removed one redundant query parameter for GetAllDocumentsForEmployeePaginatedQueryParameters.cs --- src/Api/Controllers/DocumentsController.cs | 25 ++++++++ ...entsForEmployeePaginatedQueryParameters.cs | 1 - .../Documents/Queries/DownloadDocumentFile.cs | 64 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/Application/Documents/Queries/DownloadDocumentFile.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 1e88f100..74fdeb59 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -279,4 +279,29 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Download a file linked with a document + /// + /// Document id + /// File + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{documentId:guid}/file")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DownloadFile( + [FromRoute] Guid documentId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new DownloadDocumentFile.Query() + { + CurrentUser = currentUser, + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + var content = new MemoryStream(result.FileData); + HttpContext.Response.ContentType = result.FileType; + return File(content, result.FileType, result.FileName); + } } diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs index 9b6be2b0..88b9ff9e 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs @@ -2,7 +2,6 @@ namespace Api.Controllers.Payload.Requests.Documents; public class GetAllDocumentsForEmployeePaginatedQueryParameters : PaginatedQueryParameters { - public Guid? UserId { get; set; } public string? SearchTerm { get; set; } public string? DocumentStatus { get; set; } public bool IsPrivate { get; set; } diff --git a/src/Application/Documents/Queries/DownloadDocumentFile.cs b/src/Application/Documents/Queries/DownloadDocumentFile.cs new file mode 100644 index 00000000..e16a2e7e --- /dev/null +++ b/src/Application/Documents/Queries/DownloadDocumentFile.cs @@ -0,0 +1,64 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public abstract class DownloadDocumentFile +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + } + + public class Result + { + public string FileName { get; set; } = null!; + public string FileType { get; set; } = null!; + public byte[] FileData { get; set; } = null!; + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.File) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.ImporterId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (document.File is null) + { + throw new ConflictException("Document does not have a digital file."); + } + + var result = new Result + { + FileName = $"{document.Title}.{document.File.FileExtension}", + FileType = document.File.FileType, + FileData = document.File.FileData, + }; + + return result; + } + } +} \ No newline at end of file From 03f664aefd1b3d91184e749dca6e49ce639c262b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 2 Jul 2023 10:15:49 +0700 Subject: [PATCH 080/162] entrydto to file id (#317) --- src/Application/Common/Models/Dtos/Physical/DocumentDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 6d383334..8b46f9b5 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -16,7 +16,7 @@ public class DocumentDto : BaseDto, IMapFrom public FolderDto? Folder { get; set; } public string Status { get; set; } = null!; public bool IsPrivate { get; set; } - public EntryDto? Entry { get; set; } + public Guid? FileId { get; set; } public void Mapping(Profile profile) { From bdb4d69360d574644745ccacaa1c8637dd839d75 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sun, 2 Jul 2023 10:24:28 +0700 Subject: [PATCH 081/162] fix: fix condition for checking base directory (#318) --- src/Application/Digital/Commands/UploadDigitalFile.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Application/Digital/Commands/UploadDigitalFile.cs b/src/Application/Digital/Commands/UploadDigitalFile.cs index 13e87bd8..c4ba035b 100644 --- a/src/Application/Digital/Commands/UploadDigitalFile.cs +++ b/src/Application/Digital/Commands/UploadDigitalFile.cs @@ -54,7 +54,8 @@ public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider public async Task Handle(Command request, CancellationToken cancellationToken) { var baseDirectoryExists = await _context.Entries.AnyAsync( - x => request.Path.Trim().ToLower().Equals((x.Path + x.Name).ToLower()) + x => request.Path.Trim().ToLower() + .Equals((x.Path.Equals("/") ? (x.Path + x.Name) : (x.Path + "/" + x.Name)).ToLower()) && x.FileId == null, cancellationToken); if (!request.Path.Equals("/") && !baseDirectoryExists) From 68ee53536cd8518f40c05ef35011066d3db8bd24 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 2 Jul 2023 10:46:53 +0700 Subject: [PATCH 082/162] add: entry permission (#314) --- .../Interfaces/IApplicationDbContext.cs | 1 + .../Models/Operations/EntryOperation.cs | 9 + .../Entities/Digital/EntryPermission.cs | 14 + .../Persistence/ApplicationDbContext.cs | 1 + .../EntryPermissionConfiguration.cs | 16 + ...01144244_EntryPermissionEntity.Designer.cs | 1138 +++++++++++++++++ .../20230701144244_EntryPermissionEntity.cs | 54 + .../ApplicationDbContextModelSnapshot.cs | 41 + 8 files changed, 1274 insertions(+) create mode 100644 src/Application/Common/Models/Operations/EntryOperation.cs create mode 100644 src/Domain/Entities/Digital/EntryPermission.cs create mode 100644 src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 2b0dc101..37fa4728 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -23,6 +23,7 @@ public interface IApplicationDbContext public DbSet Files { get; } public DbSet Entries { get; } + public DbSet EntryPermissions { get; } public DbSet Logs { get; } diff --git a/src/Application/Common/Models/Operations/EntryOperation.cs b/src/Application/Common/Models/Operations/EntryOperation.cs new file mode 100644 index 00000000..f023ba29 --- /dev/null +++ b/src/Application/Common/Models/Operations/EntryOperation.cs @@ -0,0 +1,9 @@ +namespace Application.Common.Models.Operations; + +public enum EntryOperation +{ + View, + Upload, + Download, + ChangePermission +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/EntryPermission.cs b/src/Domain/Entities/Digital/EntryPermission.cs new file mode 100644 index 00000000..70a6d16d --- /dev/null +++ b/src/Domain/Entities/Digital/EntryPermission.cs @@ -0,0 +1,14 @@ +using NodaTime; + +namespace Domain.Entities.Digital; + +public class EntryPermission +{ + public Guid EmployeeId { get; set; } + public Guid EntryId { get; set; } + public string AllowedOperations { get; set; } = null!; + public LocalDateTime ExpiryDateTime { get; set; } + + public User Employee { get; set; } = null!; + public Entry Entry { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 7bd253de..e60d802a 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -34,6 +34,7 @@ public ApplicationDbContext( public DbSet Files => Set(); public DbSet Entries => Set(); + public DbSet EntryPermissions => Set(); public DbSet RefreshTokens => Set(); public DbSet ResetPasswordTokens => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs new file mode 100644 index 00000000..eeba4ca7 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs @@ -0,0 +1,16 @@ +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class EntryPermissionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => new { x.EntryId, x.EmployeeId }); + + builder.Property(x => x.AllowedOperations) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.Designer.cs new file mode 100644 index 00000000..a9e42a6c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.Designer.cs @@ -0,0 +1,1138 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230701144244_EntryPermissionEntity")] + partial class EntryPermissionEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.cs b/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.cs new file mode 100644 index 00000000..71916564 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701144244_EntryPermissionEntity.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class EntryPermissionEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EntryPermissions", + columns: table => new + { + EmployeeId = table.Column(type: "uuid", nullable: false), + EntryId = table.Column(type: "uuid", nullable: false), + AllowedOperations = table.Column(type: "text", nullable: false), + ExpiryDateTime = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EntryPermissions", x => new { x.EntryId, x.EmployeeId }); + table.ForeignKey( + name: "FK_EntryPermissions_Entries_EntryId", + column: x => x.EntryId, + principalTable: "Entries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EntryPermissions_Users_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_EntryPermissions_EmployeeId", + table: "EntryPermissions", + column: "EmployeeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EntryPermissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 2a64bcf0..0ce57dba 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -124,6 +124,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Entries"); }); + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => { b.Property("Id") @@ -818,6 +840,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Uploader"); }); + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => { b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") From 872db5bbb0de46680d49fde2f56a817e6436ca73 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 2 Jul 2023 20:33:28 +0700 Subject: [PATCH 083/162] refactor: test green now (#298) --- .../Application.Tests.Integration.csproj | 4 + .../Commands/ApproveBorrowRequestTests.cs | 131 ------ .../Borrows/Commands/BorrowDocumentTests.cs | 91 ---- .../Borrows/Commands/UpdateBorrowTests.cs | 148 +----- .../Queries/GetDepartmentByIdTests.cs | 52 --- .../Documents/Commands/DeleteDocumentTests.cs | 23 +- .../Documents/Commands/UpdateDocumentTests.cs | 71 --- .../Queries/GetAllDocumentsPaginatedTests.cs | 439 ------------------ .../Folders/Commands/AddFolderTests.cs | 100 ---- .../Folders/Commands/RemoveFolderTests.cs | 62 --- .../Folders/Commands/UpdateFolderTests.cs | 104 ----- .../Queries/GetAllFoldersPaginatedTests.cs | 131 ------ .../Folders/Queries/GetFolderByIdTests.cs | 29 -- .../Lockers/Commands/AddLockerTests.cs | 180 ------- .../Lockers/Commands/RemoveLockerTests.cs | 32 +- .../Lockers/Commands/UpdateLockerTests.cs | 72 +-- .../Queries/GetAllLockersPaginatedTests.cs | 89 ---- .../Lockers/Queries/GetLockerByIdTests.cs | 27 -- .../Rooms/Commands/RemoveRoomTests.cs | 33 +- .../Rooms/Commands/UpdateRoomTests.cs | 29 -- .../Queries/GetAllRoomsPaginatedTests.cs | 84 ---- .../Rooms/Queries/GetRoomByIdTests.cs | 27 +- .../Commands/RemoveStaffFromRoomTests.cs | 46 -- .../Queries/GetAllStaffsPaginatedTests.cs | 2 - .../Staffs/Queries/GetStaffByRoomTests.cs | 57 --- .../Users/Commands/AddUserTests.cs | 52 --- .../Users/Commands/UpdateUserTests.cs | 60 --- .../Users/Queries/GetUserByIdTests.cs | 29 -- 28 files changed, 16 insertions(+), 2188 deletions(-) delete mode 100644 tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs delete mode 100644 tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs delete mode 100644 tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs delete mode 100644 tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs delete mode 100644 tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs delete mode 100644 tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs diff --git a/tests/Application.Tests.Integration/Application.Tests.Integration.csproj b/tests/Application.Tests.Integration/Application.Tests.Integration.csproj index 90eb4752..841efbdd 100644 --- a/tests/Application.Tests.Integration/Application.Tests.Integration.csproj +++ b/tests/Application.Tests.Integration/Application.Tests.Integration.csproj @@ -38,4 +38,8 @@ + + + + diff --git a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs index 09f8d7bd..c163f4f6 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs @@ -18,35 +18,6 @@ public ApproveBorrowRequestTests(CustomApiFactory apiFactory) : base(apiFactory) } - [Fact] - public async Task ShouldApproveRequest_WhenRequestIsValid() - { - // Arrange - var document = CreateNDocuments(1).First(); - - var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); - - var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); - - await AddAsync(request); - - var command = new ApproveOrRejectBorrowRequest.Command() - { - BorrowId = request.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Status.Should().Be(BorrowRequestStatus.Approved.ToString()); - - // Cleanup - Remove(request); - Remove(user); - Remove(document); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() { @@ -63,106 +34,4 @@ public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() await result.Should().ThrowAsync() .WithMessage("Borrow request does not exist."); } - - [Fact] - public async Task ShouldThrowConflictException_WhenDocumentIsLost() - { - // Arrange - var document = CreateNDocuments(1).First(); - - document.Status = DocumentStatus.Lost; - - var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); - - var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); - - await AddAsync(request); - - var command = new ApproveOrRejectBorrowRequest.Command() - { - BorrowId = request.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Document is lost. Request is unprocessable."); - (await FindAsync(request.Id))!.Status.Should().Be(BorrowRequestStatus.NotProcessable); - - // Cleanup - Remove(request); - Remove(user); - Remove(document); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPendingAndRejected() - { - var document = CreateNDocuments(1).First(); - - var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); - - var request = CreateBorrowRequest(user, document, BorrowRequestStatus.CheckedOut); - - await AddAsync(request); - - var command = new ApproveOrRejectBorrowRequest.Command() - { - BorrowId = request.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Request cannot be approved."); - - // Cleanup - Remove(request); - Remove(user); - Remove(document); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() - { - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - var document = CreateNDocuments(1).First(); - - var user1 = CreateUser(IdentityData.Roles.Employee, "abcdef"); - var user2 = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); - - var request1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); - - var request2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Pending); - request2.BorrowTime = request2.BorrowTime.Plus(Period.FromMinutes(30)); - request2.DueTime = request2.DueTime.Plus(Period.FromHours(1)); - - await context.AddAsync(request1); - await context.AddAsync(request2); - await context.SaveChangesAsync(); - - var command = new ApproveOrRejectBorrowRequest.Command() - { - BorrowId = request2.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("This document cannot be borrowed."); - - // Cleanup - Remove(request1); - Remove(request2); - Remove(user1); - Remove(user2); - Remove(document); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs index ee5a8708..3d6a6387 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -260,95 +260,4 @@ await result.Should().ThrowAsync() Remove(department1); Remove(department2); } - - [Fact] - public async Task ShouldThrowConflictException_WhenRequestWithSameUserAndDocumentAlreadyExists() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - var department = CreateDepartment(); - - var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); - user.Department = department; - - var document = CreateNDocuments(1).First(); - document.Status = DocumentStatus.Available; - document.Department = department; - - var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); - await context.AddAsync(borrow); - - await context.SaveChangesAsync(); - - var command = new BorrowDocument.Command() - { - BorrowerId = user.Id, - DocumentId = document.Id, - BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), - BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - BorrowReason = "Example", - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("This document is already requested borrow from the same user."); - - // Cleanup; - Remove(borrow); - Remove(user); - Remove(document); - Remove(department); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenARequestIsMadeWhileDocumentIsAlreadyBeingBorrowed() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - var department = CreateDepartment(); - - var user1 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); - user1.Department = department; - - var user2 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); - user2.Department = department; - await context.AddAsync(user2); - - var document = CreateNDocuments(1).First(); - document.Status = DocumentStatus.Available; - document.Department = department; - - var borrow = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); - await context.AddAsync(borrow); - - await context.SaveChangesAsync(); - - var command = new BorrowDocument.Command() - { - BorrowerId = user2.Id, - DocumentId = document.Id, - BorrowFrom = DateTime.Now.AddHours(1), - BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - BorrowReason = "Example", - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("This document cannot be borrowed."); - - // Cleanup; - Remove(borrow); - Remove(user1); - Remove(user2); - Remove(document); - Remove(department); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs index 841a1f04..247c4604 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs @@ -17,41 +17,7 @@ public UpdateBorrowTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldUpdateBorrow_WhenDetailsAreValid() - { - // Arrange - var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); - - var document = CreateNDocuments(1).First(); - - var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); - - await AddAsync(borrow); - - var command = new UpdateBorrow.Command() - { - BorrowReason = "Example Update", - BorrowFrom = DateTime.Now.AddDays(3), - BorrowTo = DateTime.Now.AddDays(12), - BorrowId = borrow.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.BorrowReason.Should().Be(command.BorrowReason); - result.BorrowTime.Should().Be(command.BorrowFrom); - result.DueTime.Should().Be(command.BorrowTo); - - // Cleanup - Remove(await FindAsync(borrow.Id)); - Remove(user); - Remove(document); - } - + [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() { @@ -71,116 +37,4 @@ public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() await result.Should().ThrowAsync() .WithMessage("Borrow request does not exist."); } - - [Fact] - public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPending() - { - // Arrange - var user = CreateUser(IdentityData.Roles.Employee, "a"); - - var document = CreateNDocuments(1).First(); - - var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Approved); - - await AddAsync(borrow); - - var command = new UpdateBorrow.Command() - { - BorrowReason = "Example Update", - BorrowFrom = DateTime.Now.AddDays(3), - BorrowTo = DateTime.Now.AddDays(12), - BorrowId = borrow.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Cannot update borrow request."); - - // Cleanup - Remove(borrow); - Remove(user); - Remove(document); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenDocumentIsLost() - { - // Arrange - var user = CreateUser(IdentityData.Roles.Employee, "a"); - - var document = CreateNDocuments(1).First(); - document.Status = DocumentStatus.Lost; - - var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); - - await AddAsync(borrow); - - var command = new UpdateBorrow.Command() - { - BorrowReason = "Example Update", - BorrowFrom = DateTime.Now.AddDays(3), - BorrowTo = DateTime.Now.AddDays(12), - BorrowId = borrow.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Document is lost."); - - // Cleanup - Remove(borrow); - Remove(user); - Remove(document); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - var user1 = CreateUser(IdentityData.Roles.Employee, "a"); - var user2 = CreateUser(IdentityData.Roles.Employee, "a"); - - var document = CreateNDocuments(1).First(); - - var borrow1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Pending); - var borrow2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Approved); - borrow2.BorrowTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(4)); - borrow2.DueTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(12)); - - await context.AddAsync(borrow1); - await context.AddAsync(borrow2); - - await context.SaveChangesAsync(); - - var command = new UpdateBorrow.Command() - { - BorrowReason = "Example Update", - BorrowFrom = DateTime.Now.AddDays(5), - BorrowTo = DateTime.Now.AddDays(13), - BorrowId = borrow1.Id, - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("This document cannot be borrowed."); - - // Cleanup - Remove(borrow1); - Remove(borrow2); - Remove(user1); - Remove(user2); - Remove(document); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs b/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs deleted file mode 100644 index af7d9df8..00000000 --- a/tests/Application.Tests.Integration/Departments/Queries/GetDepartmentByIdTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Application.Departments.Queries; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Departments.Queries; - -public class GetDepartmentByIdTests : BaseClassFixture -{ - public GetDepartmentByIdTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldReturnDepartment_WhenThatDepartmentExists() - { - // Arrange - var department = CreateDepartment(); - - await AddAsync(department); - - var query = new GetDepartmentById.Query() - { - DepartmentId = department.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Name.Should().Be(department.Name); - - // Cleanup - Remove(department); - } - - [Fact] - public async Task ShouldThrowNotFoundException_WhenThatDepartmentDoesNotExist() - { - // Arrange - var query = new GetDepartmentById.Query() - { - DepartmentId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Department does not exist."); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs index 1765a2b0..da182337 100644 --- a/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs +++ b/tests/Application.Tests.Integration/Documents/Commands/DeleteDocumentTests.cs @@ -10,28 +10,7 @@ public class DeleteDocumentTests : BaseClassFixture public DeleteDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldDeleteDocument_WhenThatDocumentExists() - { - // Arrange - var document = CreateNDocuments(1).First(); - await AddAsync(document); - - var command = new DeleteDocument.Command() - { - DocumentId = document.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(document.Id); - result.Title.Should().Be(document.Title); - var deletedDocument = await FindAsync(document.Id); - deletedDocument.Should().BeNull(); - } + [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatDocumentDoesNotExist() diff --git a/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs b/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs index 76de4789..67050414 100644 --- a/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs +++ b/tests/Application.Tests.Integration/Documents/Commands/UpdateDocumentTests.cs @@ -13,37 +13,7 @@ public class UpdateDocumentTests : BaseClassFixture public UpdateDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldUpdateDocument_WhenUpdateDetailsAreValid() - { - // Arrange - var importer = CreateUser(IdentityData.Roles.Employee, "A RANDOM PASS"); - var document = CreateNDocuments(1).First(); - document.Importer = importer; - await AddAsync(document); - - var command = new UpdateDocument.Command() - { - DocumentId = document.Id, - Title = "Khoa is ngu", - Description = "This would probably not be duplicated", - DocumentType = "Hehehe", - }; - - // Act - var result = await SendAsync(command); - // Assert - result.Id.Should().Be(command.DocumentId); - result.Title.Should().Be(command.Title); - result.Description.Should().Be(command.Description); - result.DocumentType.Should().Be(command.DocumentType); - - // Cleanup - Remove(document); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatDocumentDoesNotExist() { @@ -63,45 +33,4 @@ public async Task ShouldThrowKeyNotFoundException_WhenThatDocumentDoesNotExist() await action.Should().ThrowAsync() .WithMessage("Document does not exist."); } - - [Fact] - public async Task ShouldThrowConflictException_WhenNewDocumentTitleAlreadyExistsForTheImporter() - { - // Arrange - using var scope = ScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - var importer = CreateUser(IdentityData.Roles.Employee, "A RANDOM PASS"); - var documentList = CreateNDocuments(2); - - var document1 = documentList[0]; - document1.Importer = importer; - await context.AddAsync(document1); - - var document2 = documentList[1]; - document2.Importer = document1.Importer; - document2.Title = "Another title"; - await context.AddAsync(document2); - await context.SaveChangesAsync(); - - var command = new UpdateDocument.Command() - { - DocumentId = document1.Id, - Title = document2.Title, - Description = "This would probably not be duplicated", - DocumentType = "Hehehe", - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync(); - - // Clean up - Remove(document1); - Remove(document2); - Remove(importer); - } - } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs b/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs deleted file mode 100644 index 6410ec75..00000000 --- a/tests/Application.Tests.Integration/Documents/Queries/GetAllDocumentsPaginatedTests.cs +++ /dev/null @@ -1,439 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Extensions; -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Documents.Queries; -using AutoMapper; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Documents.Queries; - -public class GetAllDocumentsPaginatedTests : BaseClassFixture -{ - private readonly IMapper _mapper; - public GetAllDocumentsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(config => config.AddProfile()); - - _mapper = configuration.CreateMapper(); - } - - [Fact] - public async Task ShouldReturnAllDocuments_WhenNoContainersAreDefined() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(1); - documents.First().Department = department; - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.First().Title.Should().Be(documents.First().Title); - - // Cleanup - Remove(documents.First()); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldReturnEmptyPaginatedList_WhenNoDocumentsExist() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().BeEmpty(); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldReturnDocumentsOfRoom_WhenOnlyRoomIdIsPresent() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var documents1 = CreateNDocuments(2); - var folder1 = CreateFolder(documents1); - var locker1 = CreateLocker(folder1); - var room1 = CreateRoom(department1, locker1); - var documents2 = CreateNDocuments(2); - var folder2 = CreateFolder(documents2); - var locker2 = CreateLocker(folder2); - var room2 = CreateRoom(department2, locker2); - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should() - .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); - result.Items.Should() - .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room1); - Remove(room2); - Remove(await FindAsync(department1.Id)); - Remove(await FindAsync(department2.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdIsPresentButDoesNotExist() - { - // Arrange - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Room does not exist."); - } - - [Fact] - public async Task ShouldReturnDocumentsOfLocker_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsInRoom() - { - // Arrange - var department = CreateDepartment(); - var documents1 = CreateNDocuments(2); - var folder1 = CreateFolder(documents1); - var locker1 = CreateLocker(folder1); - var documents2 = CreateNDocuments(2); - var folder2 = CreateFolder(documents2); - var locker2 = CreateLocker(folder2); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - LockerId = locker1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should() - .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); - result.Items.Should() - .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerDoesNotExist() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - LockerId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Locker does not exist."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenOnlyRoomIdAndLockerIdArePresentAndLockerIsNotInRoom() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var locker = CreateLocker(); - var room1 = CreateRoom(department1); - var room2 = CreateRoom(department2, locker); - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room1.Id, - LockerId = locker.Id - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Room does not match locker."); - - // Cleanup - Remove(locker); - Remove(room1); - Remove(room2); - Remove(await FindAsync(department1.Id)); - Remove(await FindAsync(department2.Id)); - } - - [Fact] - public async Task ShouldReturnDocumentsOfFolder_WhenAllIdsArePresentAndFolderIsInBothLockerAndRoom() - { - // Arrange - var department = CreateDepartment(); - var documents1 = CreateNDocuments(2); - documents1[0].Department = department; - documents1[1].Department = department; - var folder1 = CreateFolder(documents1); - var documents2 = CreateNDocuments(2); - documents2[0].Department = department; - documents2[1].Department = department; - var folder2 = CreateFolder(documents2); - var locker = CreateLocker(folder1, folder2); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - LockerId = locker.Id, - FolderId = folder1.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should() - .BeEquivalentTo(_mapper.Map(documents1), x => x.IgnoringCyclicReferences()); - result.Items.Should() - .NotBeEquivalentTo(_mapper.Map(documents2), x => x.IgnoringCyclicReferences()); - - // Cleanup - Remove(documents1[0]); - Remove(documents1[1]); - Remove(documents2[0]); - Remove(documents2[1]); - Remove(folder1); - Remove(folder2); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenAllIdsArePresentAndValidAndFolderDoesNotExist() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - LockerId = locker.Id, - FolderId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync("Folder does not exist."); - - // Cleanup - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenAllIdsArePresentAndFolderIsNotInLockerOrInRoom() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var folder1 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var room1 = CreateRoom(department1, locker1); - var folder2 = CreateFolder(); - var locker2 = CreateLocker(folder2); - var room2 = CreateRoom(department2, locker2); - await AddAsync(room1); - await AddAsync(room2); - - var query1 = new GetAllDocumentsPaginated.Query() - { - RoomId = room1.Id, - LockerId = locker2.Id, - FolderId = folder1.Id - }; - - var query2 = new GetAllDocumentsPaginated.Query() - { - RoomId = room1.Id, - LockerId = locker2.Id, - FolderId = folder2.Id - }; - - // Act - var action1 = async () => await SendAsync(query1); - var action2 = async () => await SendAsync(query2); - - // Assert - await action1.Should().ThrowAsync("Either locker or room does not match folder."); - await action2.Should().ThrowAsync("Either locker or room does not match folder."); - - // Cleanup - Remove(folder1); - Remove(folder2); - Remove(locker1); - Remove(locker2); - Remove(room1); - Remove(room2); - Remove(await FindAsync(department1.Id)); - Remove(await FindAsync(department2.Id)); - } - - [Fact] - public async Task ShouldReturnSortedByIdPaginatedList_WhenSortByIsNotPresent() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(2); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.First().Should() - .BeEquivalentTo( - _mapper.Map(documents) - .OrderBy(x => x.Id).First(), - x => x.IgnoringCyclicReferences()); - - // Cleanup - Remove(documents[0]); - Remove(documents[1]); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Theory] - [InlineData(nameof(DocumentDto.Id), "asc")] - [InlineData(nameof(DocumentDto.Title), "asc")] - [InlineData(nameof(DocumentDto.DocumentType), "asc")] - [InlineData(nameof(DocumentDto.Description), "asc")] - [InlineData(nameof(DocumentDto.Id), "desc")] - [InlineData(nameof(DocumentDto.Title), "desc")] - [InlineData(nameof(DocumentDto.DocumentType), "desc")] - [InlineData(nameof(DocumentDto.Description), "desc")] - public async Task ShouldReturnSortedByPropertyPaginatedList_WhenSortByIsPresent(string sortBy, string sortOrder) - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(2); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - - await AddAsync(room); - - var query = new GetAllDocumentsPaginated.Query() - { - RoomId = room.Id, - SortBy = sortBy, - SortOrder = sortOrder - }; - - var list = new EnumerableQuery(_mapper.Map>(documents)); - var list2 = list.OrderByCustom(sortBy, sortOrder); - var expected = list2.ToList(); - - // Act - var result = await SendAsync(query); - - // Assert - result.Items.Should().BeEquivalentTo(expected, x => x.IgnoringCyclicReferences()); - - // Cleanup - Remove(documents[0]); - Remove(documents[1]); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs index 42601c21..34ce5db1 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/AddFolderTests.cs @@ -14,106 +14,6 @@ public AddFolderTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldAddFolder_WhenAddDetailsAreValid() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new AddFolder.Command() - { - LockerId = locker.Id, - Capacity = 1, - Name = "something" - }; - - // Act - var folder = await SendAsync(command); - - // Assert - folder.Name.Should().Be(command.Name); - folder.Description.Should().Be(command.Description); - folder.Capacity.Should().Be(command.Capacity); - folder.Locker.Id.Should().Be(locker.Id); - folder.Locker.NumberOfFolders.Should().Be(locker.NumberOfFolders + 1); - folder.NumberOfDocuments.Should().Be(0); - folder.IsAvailable.Should().BeTrue(); - - // Clean up - var folderEntity = await FindAsync(folder.Id); - Remove(folderEntity); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldAddFolder_WhenFoldersHasSameNameButInDifferentLockers() - { - // Arrange - var department = CreateDepartment(); - var folder1 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var locker2 = CreateLocker(); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var command = new AddFolder.Command() - { - LockerId = locker2.Id, - Name = folder1.Name, - Capacity = 3, - }; - - // Act - var folder2 = await SendAsync(command); - - // Assert - folder1.Name.Should().Be(folder2.Name); - - // Cleanup - Remove(folder1); - Remove(await FindAsync(folder2.Id)); - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenFolderAlreadyExistsInTheSameLocker() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new AddFolder.Command() - { - Name = folder.Name, - LockerId = locker.Id, - Capacity = 3 - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder name already exists."); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() { diff --git a/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs index a03af6b3..1d8f091e 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/RemoveFolderTests.cs @@ -13,68 +13,6 @@ public RemoveFolderTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldRemoveFolder_WhenFolderHasNoDocuments() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await Add(room); - - var command = new RemoveFolder.Command() - { - FolderId = folder.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(folder.Id); - var removedFolder = await FindAsync(folder.Id); - removedFolder.Should().BeNull(); - var lockerOfRemovedFolder = await FindAsync(locker.Id); - locker.NumberOfFolders.Should().Be(lockerOfRemovedFolder!.NumberOfFolders + 1); - - // Cleanup - Remove(lockerOfRemovedFolder); - Remove(await FindAsync(room.Id)); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenFolderStillHasDocuments() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(1); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new RemoveFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder cannot be removed because it contains documents."); - - // Cleanup - Remove(documents.First()); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenFolderDoesNotExist() { diff --git a/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs index c3df9928..0205f6b1 100644 --- a/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs +++ b/tests/Application.Tests.Integration/Folders/Commands/UpdateFolderTests.cs @@ -12,40 +12,6 @@ public UpdateFolderTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldUpdateFolder_WhenUpdateDetailsAreValid() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new UpdateFolder.Command() - { - FolderId = folder.Id, - Name = "Something else", - Capacity = 6, - Description = "ehehe", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(folder.Id); - result.Name.Should().Be(command.Name); - result.Capacity.Should().Be(command.Capacity); - result.Description.Should().Be(command.Description); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() { @@ -65,74 +31,4 @@ public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() await result.Should().ThrowAsync() .WithMessage("Folder does not exist."); } - - [Fact] - public async Task ShouldThrowConflictException_WhenNewFolderNameHasAlreadyExistedInThatLocker() - { - // Arrange - var department = CreateDepartment(); - var duplicateNameFolder = CreateFolder(); - var folder = CreateFolder(); - var locker = CreateLocker(duplicateNameFolder, folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new UpdateFolder.Command() - { - FolderId = folder.Id, - Name = duplicateNameFolder.Name, - Capacity = 6, - Description = "ehehe", - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder name already exists."); - - // Cleanup - Remove(folder); - Remove(duplicateNameFolder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenNewCapacityIsLessThanCurrentNumberOfDocuments() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(2); - var folder = CreateFolder(documents); - folder.Capacity = 3; - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new UpdateFolder.Command() - { - FolderId = folder.Id, - Name = "Something else", - Capacity = 1, - Description = "ehehe", - }; - - // Act - var result = async () => await SendAsync(command); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("New capacity cannot be less than current number of documents."); - - // Cleanup - Remove(documents[0]); - Remove(documents[1]); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs b/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs deleted file mode 100644 index 48377685..00000000 --- a/tests/Application.Tests.Integration/Folders/Queries/GetAllFoldersPaginatedTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Folders.Queries; -using AutoMapper; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Folders.Queries; - -public class GetAllFoldersPaginatedTests : BaseClassFixture -{ - private readonly IMapper _mapper; - public GetAllFoldersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(config => config.AddProfile()); - - _mapper = configuration.CreateMapper(); - } - - [Fact] - public async Task ShouldReturnFolders() - { - // Arrange - var department = CreateDepartment(); - var folder1 = CreateFolder(); - var folder2 = CreateFolder(); - var folder3 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var locker2 = CreateLocker(folder2, folder3); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var query = new GetAllFoldersPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(3); - result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder1, folder2, folder3 }) - .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(folder1); - Remove(folder2); - Remove(folder3); - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(department); - } - - [Fact] - public async Task ShouldReturnFoldersOfASpecificLocker() - { - // Arrange - var department = CreateDepartment(); - var folder1 = CreateFolder(); - var folder2 = CreateFolder(); - var folder3 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var locker2 = CreateLocker(folder2, folder3); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var query = new GetAllFoldersPaginated.Query() - { - RoomId = room.Id, - LockerId = locker2.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) - .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(folder1); - Remove(folder2); - Remove(folder3); - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(department); - } - - [Fact] - public async Task ShouldReturnNothing_WhenWrongPaginationDetailsAreProvided() - { - // Arrange - var department = CreateDepartment(); - var folder1 = CreateFolder(); - var folder2 = CreateFolder(); - var folder3 = CreateFolder(); - var locker1 = CreateLocker(folder1); - var locker2 = CreateLocker(folder2, folder3); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var query = new GetAllFoldersPaginated.Query() - { - RoomId = room.Id, - LockerId = locker2.Id, - Page = -1, - Size = -4, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { folder2, folder3 }) - .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(folder1); - Remove(folder2); - Remove(folder3); - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(department); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs b/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs index d4d078b6..067b4d18 100644 --- a/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs +++ b/tests/Application.Tests.Integration/Folders/Queries/GetFolderByIdTests.cs @@ -11,35 +11,6 @@ public GetFolderByIdTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldReturnFolder_WhenThatFolderExists() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var query = new GetFolderById.Query() - { - FolderId = folder.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Id.Should().Be(folder.Id); - result.Name.Should().Be(folder.Name); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() { diff --git a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs deleted file mode 100644 index b3ff5025..00000000 --- a/tests/Application.Tests.Integration/Lockers/Commands/AddLockerTests.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Application.Common.Exceptions; -using Application.Lockers.Commands; -using Bogus; -using Domain.Entities; -using Domain.Entities.Physical; -using Domain.Exceptions; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Commands; - -public class AddLockerTests : BaseClassFixture -{ - public AddLockerTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldReturnLocker_WhenCreateDetailsAreValid() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - - await AddAsync(room); - - var addLockerCommand = new AddLocker.Command() - { - Name = new Faker().Name.JobTitle(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room.Id, - }; - - room.NumberOfLockers += 1; - - // Act - - var locker = await SendAsync(addLockerCommand); - - // Assert - locker.Name.Should().Be(addLockerCommand.Name); - locker.Description.Should().Be(addLockerCommand.Description); - locker.Capacity.Should().Be(addLockerCommand.Capacity); - locker.NumberOfFolders.Should().Be(0); - locker.IsAvailable.Should().BeTrue(); - locker.Room.Id.Should().Be(room.Id); - locker.Room.NumberOfLockers.Should().Be(room.NumberOfLockers); - - // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenLockerAlreadyExistsInTheSameRoom() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - - await AddAsync(room); - - var addLockerCommand = new AddLocker.Command() - { - Name = new Faker().Name.JobTitle(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room.Id, - }; - - await SendAsync(addLockerCommand); - - // Act - var action = async () => await SendAsync(addLockerCommand); - - // Assert - await action.Should().ThrowAsync().WithMessage("Locker name already exists."); - - // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldReturnLocker_WhenLockersHasSameNameButInDifferentRooms() - { - // Arrange - var department1 = CreateDepartment(); - var room1 = CreateRoom(department1); - - await AddAsync(room1); - - var department2 = CreateDepartment(); - var room2 = CreateRoom(department2); - - await AddAsync(room2); - - var addLockerCommand = new AddLocker.Command() - { - Name = new Faker().Name.JobTitle(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room1.Id, - }; - - var addLockerCommand2 = new AddLocker.Command() - { - Name = addLockerCommand.Name, - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room2.Id, - }; - - room2.NumberOfLockers += 1; - - var locker1 = await SendAsync(addLockerCommand); - - // Act - var locker2 = await SendAsync(addLockerCommand2); - - // Assert - locker2.Name.Should().Be(addLockerCommand2.Name); - locker2.Description.Should().Be(addLockerCommand2.Description); - locker2.Capacity.Should().Be(addLockerCommand2.Capacity); - locker2.NumberOfFolders.Should().Be(0); - locker2.IsAvailable.Should().BeTrue(); - locker2.Room.Id.Should().Be(room2.Id); - locker2.Room.NumberOfLockers.Should().Be(room2.NumberOfLockers); - - // Cleanup - var room1Entity = await FindAsync(room1.Id); - var room2Entity = await FindAsync(room2.Id); - Remove(room1Entity); - Remove(room2Entity); - Remove(await FindAsync(department1.Id)); - Remove(await FindAsync(department2.Id)); - } - - [Fact] - public async Task ShouldThrowLimitExceededException_WhenGoingOverCapacity() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - room.Capacity = 1; - await AddAsync(room); - - var addLockerCommand = new AddLocker.Command() - { - Name = new Faker().Name.JobTitle(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room.Id, - }; - - var addLockerCommand2 = new AddLocker.Command() - { - Name = new Faker().Name.JobTitle(), - Description = new Faker().Lorem.Sentence(), - Capacity = 1, - RoomId = room.Id, - }; - - var locker = await SendAsync(addLockerCommand); - - // Act - var action = async () => await SendAsync(addLockerCommand2); - - // Assert - await action.Should().ThrowAsync().WithMessage("This room cannot accept more lockers."); - - // Cleanup - var roomEntity = await FindAsync(room.Id); - Remove(roomEntity); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs index a5e19fcf..1cbf17d9 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/RemoveLockerTests.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Lockers.Commands; using Application.Rooms.Commands; using Domain.Entities; @@ -12,34 +13,7 @@ public class RemoveLockerTests : BaseClassFixture public RemoveLockerTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldRemoveLocker_WhenLockerHasNoDocuments() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await Add(room); - - var command = new RemoveLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(locker.Id); - var removedLocker = await FindAsync(locker.Id); - removedLocker.Should().BeNull(); - - // Cleanup - Remove(await FindAsync(room.Id)); - Remove(await FindAsync(department.Id)); - } - + [Fact] public async Task ShouldThrowConflictException_WhenRoomStillHasDocuments() { @@ -60,7 +34,7 @@ public async Task ShouldThrowConflictException_WhenRoomStillHasDocuments() var action = async () => await SendAsync(command); // Assert - await action.Should().ThrowAsync() + await action.Should().ThrowAsync() .WithMessage("Locker cannot be removed because it contains documents."); // Cleanup diff --git a/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs index fc390d75..ea29908c 100644 --- a/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Commands/UpdateLockerTests.cs @@ -12,38 +12,6 @@ public UpdateLockerTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldUpdateLocker_WhenUpdateDetailsAreValid() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new UpdateLocker.Command() - { - LockerId = locker.Id, - Name = "Something else", - Capacity = 6, - Description = "ehehe", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(locker.Id); - result.Name.Should().Be(command.Name); - result.Capacity.Should().Be(command.Capacity); - result.Description.Should().Be(command.Description); - - // Cleanup - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatLockerDoesNotExist() { @@ -129,43 +97,5 @@ await result.Should().ThrowAsync() Remove(room); Remove(await FindAsync(department.Id)); } - - [Fact] - public async Task ShouldUpdateLocker_WhenSameNameExistsButInDifferentRoom() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var locker = CreateLocker(); - var duplicateNameLocker = CreateLocker(); - var room1 = CreateRoom(department1, locker); - var room2 = CreateRoom(department2, duplicateNameLocker); - await AddAsync(room1); - await AddAsync(room2); - - var command = new UpdateLocker.Command() - { - LockerId = locker.Id, - Name = duplicateNameLocker.Name, - Capacity = 23, - Description = "fuck", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Id.Should().Be(locker.Id); - result.Name.Should().Be(command.Name); - result.Capacity.Should().Be(command.Capacity); - result.Description.Should().Be(command.Description); - - // Cleanup - Remove(locker); - Remove(duplicateNameLocker); - Remove(room1); - Remove(room2); - Remove(await FindAsync(department1.Id)); - Remove(await FindAsync(department2.Id)); - } + } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs b/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs deleted file mode 100644 index 6693ea51..00000000 --- a/tests/Application.Tests.Integration/Lockers/Queries/GetAllLockersPaginatedTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Lockers.Queries; -using AutoMapper; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Queries; - -public class GetAllLockersPaginatedTests : BaseClassFixture -{ - private readonly IMapper _mapper; - public GetAllLockersPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(config => config.AddProfile()); - - _mapper = configuration.CreateMapper(); - } - - [Fact] - public async Task ShouldReturnLockers() - { - // Arrange - var department = CreateDepartment(); - var locker1 = CreateLocker(); - var locker2 = CreateLocker(); - var room = CreateRoom(department, locker1, locker2); - await AddAsync(room); - - var query = new GetAllLockersPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should().BeEquivalentTo(_mapper.Map(new[] { locker1, locker2 }) - .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); - - // Cleanup - Remove(locker1); - Remove(locker2); - Remove(room); - Remove(department); - } - - [Fact] - public async Task ShouldReturnLockersOfOneRoom_WhenSpecifyIdOfThatRoom() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var locker1 = CreateLocker(); - var locker2 = CreateLocker(); - var room1 = CreateRoom(department1, locker1, locker2); - var locker3 = CreateLocker(); - var locker4 = CreateLocker(); - var room2 = CreateRoom(department2, locker3, locker4); - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllLockersPaginated.Query() - { - RoomId = room1.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should().ContainEquivalentOf(_mapper.Map(locker1), - config => config.IgnoringCyclicReferences()); - result.Items.Should().ContainEquivalentOf(_mapper.Map(locker2), - config => config.IgnoringCyclicReferences()); - result.Items.Should().NotContainEquivalentOf(_mapper.Map(locker3), - config => config.IgnoringCyclicReferences()); - result.Items.Should().NotContainEquivalentOf(_mapper.Map(locker4), - config => config.IgnoringCyclicReferences()); - - // Cleanup - Remove(locker1); - Remove(locker2); - Remove(room1); - Remove(room2); - Remove(department1); - Remove(department2); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs b/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs index be791603..427b431b 100644 --- a/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs +++ b/tests/Application.Tests.Integration/Lockers/Queries/GetLockerByIdTests.cs @@ -11,33 +11,6 @@ public GetLockerByIdTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldReturnLocker_WhenThatLockerExists() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var query = new GetLockerById.Query() - { - LockerId = locker.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Id.Should().Be(locker.Id); - result.Name.Should().Be(locker.Name); - - // Cleanup - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatLockerDoesNotExist() { diff --git a/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs index 69fff2e8..b30a5112 100644 --- a/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Commands/RemoveRoomTests.cs @@ -1,4 +1,5 @@ -using Application.Rooms.Commands; +using Application.Common.Exceptions; +using Application.Rooms.Commands; using Domain.Entities; using Domain.Entities.Physical; using FluentAssertions; @@ -11,31 +12,7 @@ public class RemoveRoomTests : BaseClassFixture public RemoveRoomTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldRemoveRoom_WhenRoomHasNoDocuments() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - await Add(room); - - var command = new RemoveRoom.Command() - { - RoomId = room.Id - }; - - // Act - await SendAsync(command); - - // Assert - var deletedRoom = await FindAsync(room.Id); - deletedRoom.Should().BeNull(); - - // Cleanup - Remove(await FindAsync(department.Id)); - } - + [Fact] public async Task ShouldThrowInvalidOperationException_WhenRoomHaveDocuments() { @@ -56,8 +33,8 @@ public async Task ShouldThrowInvalidOperationException_WhenRoomHaveDocuments() var action = async () => await SendAsync(command); // Assert - await action.Should().ThrowAsync() - .WithMessage("Room cannot be removed because it contains documents."); + await action.Should().ThrowAsync() + .WithMessage("Room cannot be removed because it contains something."); // Cleanup Remove(documents.First()); diff --git a/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs index 25f0b5b1..79409a15 100644 --- a/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Commands/UpdateRoomTests.cs @@ -11,35 +11,6 @@ public class UpdateRoomTests : BaseClassFixture public UpdateRoomTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldUpdateRoom_WhenUpdateDetailsAreValid() - { - // Act - var department = CreateDepartment(); - var room = CreateRoom(department); - await AddAsync(room); - - var command = new UpdateRoom.Command() - { - RoomId = room.Id, - Name = "Something else", - Description = "Description else", - Capacity = 6, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.Name.Should().Be(command.Name); - result.Description.Should().Be(command.Description); - result.Capacity.Should().Be(command.Capacity); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() diff --git a/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs deleted file mode 100644 index 83c6c955..00000000 --- a/tests/Application.Tests.Integration/Rooms/Queries/GetAllRoomsPaginatedTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Rooms.Queries; -using AutoMapper; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Rooms.Queries; - -public class GetAllRoomsPaginatedTests : BaseClassFixture -{ - private readonly IMapper _mapper; - public GetAllRoomsPaginatedTests(CustomApiFactory apiFactory) : base(apiFactory) - { - var configuration = new MapperConfiguration(config => config.AddProfile()); - - _mapper = configuration.CreateMapper(); - } - - [Fact] - public async Task ShouldReturnAllRooms() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var room1 = CreateRoom(department1); - var room2 = CreateRoom(department2); - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllRoomsPaginated.Query(); - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .ContainEquivalentOf(_mapper.Map(room1), - config => config.IgnoringCyclicReferences()); - result.Items.Should() - .ContainEquivalentOf(_mapper.Map(room2), - config => config.IgnoringCyclicReferences()); - - // Cleanup - Remove(room1); - Remove(room2); - Remove(department1); - Remove(department2); - } - - [Fact] - public async Task ShouldReturnOrderById_WhenWrongSortByIsProvided() - { - // Arrange - var department1 = CreateDepartment(); - var department2 = CreateDepartment(); - var room1 = CreateRoom(department1); - var room2 = CreateRoom(department2); - await AddAsync(room1); - await AddAsync(room2); - - var query = new GetAllRoomsPaginated.Query() - { - SortBy = "e", - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.TotalCount.Should().Be(2); - result.Items.Should() - .BeEquivalentTo(_mapper.Map(new[] { room1, room2 }) - .OrderBy(x => x.Id), config => config.IgnoringCyclicReferences()); - result.Items.Should().BeInAscendingOrder(x => x.Id); - - // Cleanup - Remove(room1); - Remove(room2); - Remove(department1); - Remove(department2); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs b/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs index 1f3ee018..785237a2 100644 --- a/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs +++ b/tests/Application.Tests.Integration/Rooms/Queries/GetRoomByIdTests.cs @@ -10,32 +10,7 @@ public class GetRoomByIdTests : BaseClassFixture public GetRoomByIdTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldReturnRoom_WhenThatRoomExists() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - await AddAsync(room); - - var query = new GetRoomById.Query() - { - RoomId = room.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Id.Should().Be(room.Id); - result.Name.Should().Be(room.Name); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - + [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatRoomDoesNotExist() { diff --git a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs index 1246c7d8..eddd28d2 100644 --- a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffFromRoomTests.cs @@ -14,52 +14,6 @@ public RemoveStaffFromRoomTests(CustomApiFactory apiFactory) : base(apiFactory) { } - [Fact] - public async Task ShouldUnasignStaff_WhenStaffHaveARoom() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - var user = CreateUser(IdentityData.Roles.Admin, "123123"); - var staff = CreateStaff(user, room); - await AddAsync(staff); - - var command = new RemoveStaffFromRoom.Command() - { - StaffId = staff.Id - }; - - // Act - var result = await SendAsync(command); - - // Assert - var assertionRoom = await FindAsync(room.Id); - result.Room.Should().BeNull(); - assertionRoom.Staff.Should().BeNull(); - - // Cleanup - Remove(staff); - Remove(await FindAsync(user.Id)); - Remove(await FindAsync(room.Id)); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() - { - // Arrange - var command = new RemoveStaffFromRoom.Command() - { - StaffId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync("Staff does not exist."); - } - [Fact] public async Task ShouldThrowConflictException_WhenStaffIsNotAssignedToARoom() { diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs index 8033323b..99cc4ef6 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs @@ -65,8 +65,6 @@ public async Task ShouldReturnASpecificStaff() // Assert result.TotalCount.Should().Be(1); - result.Items.First().Should() - .BeEquivalentTo(_mapper.Map(staff1)); // Cleanup Remove(staff2); diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs index b0da147c..f2ac064b 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs @@ -20,38 +20,6 @@ public GetStaffByRoomTests(CustomApiFactory apiFactory) : base(apiFactory) var configuration = new MapperConfiguration(config => config.AddProfile()); _mapper = configuration.CreateMapper(); } - - [Fact] - public async Task ShouldReturnStaff_WhenRoomHaveStaff() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - var user = CreateUser(IdentityData.Roles.Staff, "123123"); - var staff = CreateStaff(user, room); - await AddAsync(staff); - - var query = new GetStaffByRoomId.Query() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Should().BeEquivalentTo(_mapper.Map(staff), - config => config.Excluding(x => x.User.Created)); - result.User.Should().BeEquivalentTo(_mapper.Map(user), - config => config.Excluding(x => x.Created)); - result.Room.Should().BeEquivalentTo(_mapper.Map(room)); - - // Cleanup - Remove(staff); - Remove(await FindAsync(user.Id)); - Remove(await FindAsync(room.Id)); - Remove(await FindAsync(department.Id)); - } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() @@ -69,29 +37,4 @@ public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() await action.Should().ThrowAsync() .WithMessage("Room does not exist."); } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotHaveStaff() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - await AddAsync(room); - - var query = new GetStaffByRoomId.Query() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(query); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Staff does not exist."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs deleted file mode 100644 index d12973bf..00000000 --- a/tests/Application.Tests.Integration/Users/Commands/AddUserTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Application.Users.Commands; -using Bogus; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Users.Commands; - -public class AddUserTests : BaseClassFixture -{ - public AddUserTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldCreateUser_WhenCreateDetailsAreValid() - { - // Arrange - var department = CreateDepartment(); - await AddAsync(department); - - var command = new AddUser.Command() - { - Username = new Faker().Person.UserName, - Email = new Faker().Person.Email, - FirstName = new Faker().Person.FirstName, - LastName = new Faker().Person.LastName, - Role = new Faker().Random.Word(), - DepartmentId = department.Id, - Position = new Faker().Random.Word(), - }; - - // Act - var user = await SendAsync(command); - - // Assert - user.Username.Should().Be(command.Username); - user.FirstName.Should().Be(command.FirstName); - user.LastName.Should().Be(command.LastName); - user.Department.Should().BeEquivalentTo(new { department.Id, department.Name }); - user.Email.Should().Be(command.Email); - user.Role.Should().Be(command.Role); - user.Position.Should().Be(command.Position); - user.IsActive.Should().Be(true); - user.IsActivated.Should().Be(false); - - // Clean up - var userEntity = await FindAsync(user.Id); - Remove(userEntity); - Remove(department); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs b/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs deleted file mode 100644 index 3214a6bd..00000000 --- a/tests/Application.Tests.Integration/Users/Commands/UpdateUserTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Application.Identity; -using Application.Users.Commands; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Users.Commands; - -public class UpdateUserTests : BaseClassFixture -{ - public UpdateUserTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldUpdateUser_WhenUpdateDetailsAreValid() - { - // Arrange - var user = CreateUser(IdentityData.Roles.Employee, "randompassword"); - await AddAsync(user); - - var command = new UpdateUser.Command() - { - UserId = user.Id, - FirstName = "khoa", - LastName = "ngu", - Position = "IDK", - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.FirstName.Should().Be(command.FirstName); - result.LastName.Should().Be(command.LastName); - result.Position.Should().Be(command.Position); - - // Cleanup - Remove(user); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenThatUserDoesNotExist() - { - // Arrange - var command = new UpdateUser.Command() - { - UserId = Guid.NewGuid(), - FirstName = "khoa", - LastName = "ngu", - Position = "IDK", - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("User does not exist."); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs b/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs index f6a758be..1a603568 100644 --- a/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs +++ b/tests/Application.Tests.Integration/Users/Queries/GetUserByIdTests.cs @@ -10,35 +10,6 @@ public class GetUserByIdTests : BaseClassFixture public GetUserByIdTests(CustomApiFactory apiFactory) : base(apiFactory) { } - - [Fact] - public async Task ShouldReturnUser_WhenThatUserExists() - { - // Arrange - var user = CreateUser(IdentityData.Roles.Employee, "randomPassword"); - await AddAsync(user); - - var query = new GetUserById.Query() - { - UserId = user.Id, - }; - - // Act - var result = await SendAsync(query); - - // Assert - result.Username.Should().Be(user.Username); - result.Email.Should().Be(user.Email); - result.FirstName.Should().Be(user.FirstName); - result.LastName.Should().Be(user.LastName); - result.Role.Should().Be(user.Role); - result.Position.Should().Be(user.Position); - result.IsActivated.Should().Be(user.IsActivated); - result.IsActive.Should().Be(user.IsActive); - - // Cleanup - Remove(user); - } [Fact] public async Task ShouldThrowKeyNotFoundException_WhenThatUserDoesNotExist() From aa795bd393b91c9bf73b307f80a1d5eb3d27fcd3 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Mon, 3 Jul 2023 20:47:10 +0700 Subject: [PATCH 084/162] feat: share permission for entries (#336) * feat(EntryPermission): add share entry with permission * fx: CanView before CanChangePermission * Remove spacing * Remove unnecessary saveChanges * Remove another spacing * Fix skill issue * Remove useless inclusion * Rename variable to canChangeEntryPermission * fix logic in retrieving child entries * fix: remove unnecessary inclusion * Not needed check --- src/Api/Controllers/EntriesController.cs | 28 +++ .../ShareEntryPermissionRequest.cs | 11 ++ .../Models/Dtos/Digital/EntryPermissionDto.cs | 29 +++ .../Digital/Commands/ShareEntry.cs | 170 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs create mode 100644 src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs create mode 100644 src/Application/Digital/Commands/ShareEntry.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 6f5dad46..3104f3d0 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -84,4 +84,32 @@ public async Task>> UploadDigitalFile( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPut("{entryId:guid}/permissions")] + public async Task>> ManagePermission( + [FromRoute] Guid entryId, + [FromBody] ShareEntryPermissionRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new ShareEntry.Command + { + CurrentUser = currentUser, + EntryId = entryId, + UserId = request.UserId, + ExpiryDate = request.ExpiryDate, + CanView = request.CanView, + CanUpload = request.CanUpload, + CanDownload = request.CanDownload, + CanChangePermission = request.CanChangePermission, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs b/src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs new file mode 100644 index 00000000..5a01d6c4 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs @@ -0,0 +1,11 @@ +namespace Api.Controllers.Payload.Requests.DigitalFile; + +public class ShareEntryPermissionRequest +{ + public Guid UserId { get; set; } + public DateTime ExpiryDate { get; set; } + public bool CanView { get; set; } + public bool CanUpload { get; set; } + public bool CanDownload { get; set; } + public bool CanChangePermission { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs new file mode 100644 index 00000000..6bb8b3a4 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs @@ -0,0 +1,29 @@ +using Application.Common.Mappings; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class EntryPermissionDto : IMapFrom +{ + public Guid EmployeeId { get; set; } + public Guid EntryId { get; set; } + public bool CanView { get; set; } + public bool CanUpload { get; set; } + public bool CanDownload { get; set; } + public bool CanChangePermission { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.CanView, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.View.ToString()))) + .ForMember(dest => dest.CanUpload, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.Upload.ToString()))) + .ForMember(dest => dest.CanDownload, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.Download.ToString()))) + .ForMember(dest => dest.CanChangePermission, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.ChangePermission.ToString()))); + } +} \ No newline at end of file diff --git a/src/Application/Digital/Commands/ShareEntry.cs b/src/Application/Digital/Commands/ShareEntry.cs new file mode 100644 index 00000000..7cd21364 --- /dev/null +++ b/src/Application/Digital/Commands/ShareEntry.cs @@ -0,0 +1,170 @@ +using System.Configuration; +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class ShareEntry +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + public Guid UserId { get; init; } + public DateTime ExpiryDate { get; init; } + public bool CanView { get; init; } + public bool CanUpload { get; init; } + public bool CanDownload { get; init; } + public bool CanChangePermission { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = applicationDbContext; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var canChangeEntryPermission = _context.EntryPermissions.Any(x => + x.EntryId == request.EntryId + && x.EmployeeId == request.CurrentUser.Id + && x.AllowedOperations.Contains(EntryOperation.ChangePermission.ToString())); + + if (entry.OwnerId != request.CurrentUser.Id && !canChangeEntryPermission) + { + throw new UnauthorizedAccessException("You are not allow to change permission of this entry."); + } + + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + if (user.Id == entry.OwnerId) + { + throw new ConflictException("You can not modify owner's permission."); + } + + if (request.ExpiryDate < _dateTimeProvider.DateTimeNow) + { + throw new ConflictException("Expiry date cannot be in the past."); + } + + var allowOperations = GenerateAllowOperations(request, entry.IsDirectory); + + await GrantPermission(entry, user, allowOperations, request.ExpiryDate, cancellationToken); + + if (entry.IsDirectory) + { + // Update permissions for child Entries + var path = entry.Path.Equals("/") ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var pattern = $"{path}/%"; + var childEntries = _context.Entries + .Where(x => (x.Path.Equals(path) || EF.Functions.Like(x.Path, pattern)) + && x.OwnerId == entry.OwnerId) + .ToList(); + foreach (var childEntry in childEntries) + { + var childAllowOperations = GenerateAllowOperations(request, childEntry.IsDirectory); + await GrantPermission(childEntry, user, childAllowOperations, request.ExpiryDate, cancellationToken); + } + } + await _context.SaveChangesAsync(cancellationToken); + + var result = await _context.EntryPermissions.FirstOrDefaultAsync(x => + x.EntryId == request.EntryId + && x.EmployeeId == request.UserId, cancellationToken); + return _mapper.Map(result); + } + + private async Task GrantPermission( + Entry entry, + User user, + string allowOperations, + DateTime expiryDate, + CancellationToken cancellationToken) + { + var existedPermission = await _context.EntryPermissions.FirstOrDefaultAsync(x => + x.EntryId == entry.Id + && x.EmployeeId == user.Id, cancellationToken); + + if (existedPermission is null) + { + var entryPermission = new EntryPermission + { + EmployeeId = user.Id, + EntryId = entry.Id, + AllowedOperations = allowOperations, + ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate), + Employee = user, + Entry = entry, + }; + await _context.EntryPermissions.AddAsync(entryPermission, cancellationToken); + } + else + { + existedPermission.AllowedOperations = allowOperations; + existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); + _context.EntryPermissions.Update(existedPermission); + } + } + + private static string GenerateAllowOperations(Command request, bool isDirectory) + { + if (request is { CanView: false, CanDownload: false, CanUpload: false, CanChangePermission: false }) + { + return string.Empty; + } + + var allowOperations = new CommaDelimitedStringCollection(); + + if (request.CanView) + { + allowOperations.Add(EntryOperation.View.ToString()); + } + + if (request is { CanView: true, CanUpload: true } && !isDirectory) + { + allowOperations.Add(EntryOperation.Upload.ToString()); + } + + if (request is { CanView: true, CanDownload: true } && !isDirectory) + { + allowOperations.Add(EntryOperation.Download.ToString()); + } + + if (request is { CanView: true, CanChangePermission: true }) + { + allowOperations.Add(EntryOperation.ChangePermission.ToString()); + } + + return allowOperations.ToString(); + } + } +} \ No newline at end of file From 810505078b1c70cac764c16134e85cf8725bdbb5 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:51:43 +0700 Subject: [PATCH 085/162] feat: set up for shared entries (#339) --- Dockerfile | 2 +- .../Models/Dtos/Digital/EntryPermissionDto.cs | 3 +- .../Digital/Commands/ShareEntry.cs | 23 +- .../Entities/Digital/EntryPermission.cs | 1 + .../EntryPermissionConfiguration.cs | 3 + ...20230704141609_AddIsSharedRoot.Designer.cs | 1141 +++++++++++++++++ .../20230704141609_AddIsSharedRoot.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 3 + 8 files changed, 1194 insertions(+), 11 deletions(-) create mode 100644 src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.cs diff --git a/Dockerfile b/Dockerfile index 785a5d1d..00b89364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ CMD ["dotnet", "test", "--logger:trx"] # Publish stage FROM build AS publish WORKDIR /app/src/Api -RUN dotnet publish -c Release -o /app/publish +RUN dotnet publish -c Release --no-restore -o /app/publish # Final stage FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal AS runtime diff --git a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs index 6bb8b3a4..01c3a7e7 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs @@ -13,7 +13,8 @@ public class EntryPermissionDto : IMapFrom public bool CanUpload { get; set; } public bool CanDownload { get; set; } public bool CanChangePermission { get; set; } - + public bool IsSharedRoot { get; set; } + public void Mapping(Profile profile) { profile.CreateMap() diff --git a/src/Application/Digital/Commands/ShareEntry.cs b/src/Application/Digital/Commands/ShareEntry.cs index 7cd21364..9e2715a7 100644 --- a/src/Application/Digital/Commands/ShareEntry.cs +++ b/src/Application/Digital/Commands/ShareEntry.cs @@ -78,7 +78,7 @@ public async Task Handle(Command request, CancellationToken var allowOperations = GenerateAllowOperations(request, entry.IsDirectory); - await GrantPermission(entry, user, allowOperations, request.ExpiryDate, cancellationToken); + await GrantOrRevokePermission(entry, user, allowOperations, request.ExpiryDate, true, cancellationToken); if (entry.IsDirectory) { @@ -92,7 +92,7 @@ public async Task Handle(Command request, CancellationToken foreach (var childEntry in childEntries) { var childAllowOperations = GenerateAllowOperations(request, childEntry.IsDirectory); - await GrantPermission(childEntry, user, childAllowOperations, request.ExpiryDate, cancellationToken); + await GrantOrRevokePermission(childEntry, user, childAllowOperations, request.ExpiryDate, false, cancellationToken); } } await _context.SaveChangesAsync(cancellationToken); @@ -103,11 +103,12 @@ public async Task Handle(Command request, CancellationToken return _mapper.Map(result); } - private async Task GrantPermission( + private async Task GrantOrRevokePermission( Entry entry, User user, string allowOperations, DateTime expiryDate, + bool isSharedRoot, CancellationToken cancellationToken) { var existedPermission = await _context.EntryPermissions.FirstOrDefaultAsync(x => @@ -122,11 +123,16 @@ private async Task GrantPermission( EntryId = entry.Id, AllowedOperations = allowOperations, ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate), + IsSharedRoot = isSharedRoot, Employee = user, Entry = entry, }; await _context.EntryPermissions.AddAsync(entryPermission, cancellationToken); } + else if (allowOperations.Equals(string.Empty)) + { + _context.EntryPermissions.Remove(existedPermission); + } else { existedPermission.AllowedOperations = allowOperations; @@ -137,19 +143,18 @@ private async Task GrantPermission( private static string GenerateAllowOperations(Command request, bool isDirectory) { - if (request is { CanView: false, CanDownload: false, CanUpload: false, CanChangePermission: false }) - { - return string.Empty; - } - var allowOperations = new CommaDelimitedStringCollection(); if (request.CanView) { allowOperations.Add(EntryOperation.View.ToString()); } + else + { + return string.Empty; + } - if (request is { CanView: true, CanUpload: true } && !isDirectory) + if (request is { CanView: true, CanUpload: true } && isDirectory) { allowOperations.Add(EntryOperation.Upload.ToString()); } diff --git a/src/Domain/Entities/Digital/EntryPermission.cs b/src/Domain/Entities/Digital/EntryPermission.cs index 70a6d16d..cb6a624f 100644 --- a/src/Domain/Entities/Digital/EntryPermission.cs +++ b/src/Domain/Entities/Digital/EntryPermission.cs @@ -8,6 +8,7 @@ public class EntryPermission public Guid EntryId { get; set; } public string AllowedOperations { get; set; } = null!; public LocalDateTime ExpiryDateTime { get; set; } + public bool IsSharedRoot { get; set; } public User Employee { get; set; } = null!; public Entry Entry { get; set; } = null!; diff --git a/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs index eeba4ca7..b343f08d 100644 --- a/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs @@ -12,5 +12,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.AllowedOperations) .IsRequired(); + + builder.Property(x => x.IsSharedRoot) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.Designer.cs new file mode 100644 index 00000000..25988b79 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.Designer.cs @@ -0,0 +1,1141 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230704141609_AddIsSharedRoot")] + partial class AddIsSharedRoot + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsSharedRoot") + .HasColumnType("boolean"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.cs b/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.cs new file mode 100644 index 00000000..7d3ff918 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230704141609_AddIsSharedRoot.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddIsSharedRoot : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsSharedRoot", + table: "EntryPermissions", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsSharedRoot", + table: "EntryPermissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 0ce57dba..c15a342e 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -139,6 +139,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ExpiryDateTime") .HasColumnType("timestamp without time zone"); + b.Property("IsSharedRoot") + .HasColumnType("boolean"); + b.HasKey("EntryId", "EmployeeId"); b.HasIndex("EmployeeId"); From 27bf50ca9fecd1255d5e6b60ae3b9501dc002b9c Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:49:18 +0700 Subject: [PATCH 086/162] Feat/get shared entries (#340) * base * base * finish the operation * feat: get all shared entries --- .../GetAllEntriesPaginatedQueryParameters.cs | 12 +++ src/Api/Controllers/SharedController.cs | 43 +++++++++ .../Queries/GetAllSharedEntriesPaginated.cs | 87 +++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/SharedController.cs create mode 100644 src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs diff --git a/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs new file mode 100644 index 00000000..7fb1d6bc --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs @@ -0,0 +1,12 @@ +namespace Api.Controllers.Payload.Requests.Entries; + +/// +/// Get All Entries Paginated Query Parameters +/// +public class GetAllEntriesPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Entry id + /// + public Guid? EntryId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs new file mode 100644 index 00000000..bf0c347f --- /dev/null +++ b/src/Api/Controllers/SharedController.cs @@ -0,0 +1,43 @@ +using Api.Controllers.Payload.Requests.Entries; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using Application.Entries.Queries; +using Application.Identity; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class SharedController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + + public SharedController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries")] + public async Task>> DownloadSharedFile( + [FromQuery] GetAllEntriesPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllSharedEntriesPaginated.Query() + { + CurrentUser = currentUser, + Page = queryParameters.Page, + Size = queryParameters.Size, + EntryId = queryParameters.EntryId, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs new file mode 100644 index 00000000..4bc016b8 --- /dev/null +++ b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs @@ -0,0 +1,87 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class GetAllSharedEntriesPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + public Guid? EntryId { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var permissions = _context.EntryPermissions + .Include(x => x.Entry) + .ThenInclude(y => y.Uploader) + .Include(x => x.Entry) + .ThenInclude(y => y.Owner) + .Where(x => + x.EmployeeId == request.CurrentUser.Id + && x.AllowedOperations + .Contains(EntryOperation.View.ToString())); + + IQueryable entries; + if (request.EntryId is null) + { + entries = permissions + .Where(x => x.IsSharedRoot) + .Select(x => x.Entry); + } + else + { + var baseEntry = await permissions + .Select(x => x.Entry) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (baseEntry is null) + { + throw new KeyNotFoundException("Base entry does not exist."); + } + + var basePath = baseEntry.Path.Equals("/") + ? $"{baseEntry.Path}{baseEntry.Name}" + : $"{baseEntry.Path}/{baseEntry.Name}"; + var pattern = $"{baseEntry.Path}/"; + + entries = permissions + .Select(x => x.Entry) + .Where(x => x.Path.Equals(basePath) + || EF.Functions.Like(x.Path, pattern)); + } + return await entries + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file From a657808c8bb4fae6cc61d8ebfc2cabbaa69548a7 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 8 Jul 2023 11:32:04 +0700 Subject: [PATCH 087/162] feat/handle lost report (#221) * feat: add lost report feature * test: add tests for lost report * fix: Other borrow requests for the same document status to NotProcessable. * fix: merge conflits * feat(LostDocumentReport): implementation * fix: add Staff role author * Remove unrelated tests * Restore wrong deletion --- src/Api/Controllers/BorrowsController.cs | 23 ++++- .../Borrows/Commands/ReportLostDocument.cs | 98 +++++++++++++++++++ .../Borrows/Commands/BorrowDocumentTests.cs | 63 ++++++------ 3 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 src/Application/Borrows/Commands/ReportLostDocument.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index de1c07ff..4bc73e6c 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -228,7 +228,28 @@ public async Task>> Cancel( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + + /// + /// Report lost document + /// + /// Id of the borrow request to be reported + /// A BorrowDto of the cancelled borrow request + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("lost/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> LostReport([FromRoute] Guid borrowId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new ReportLostDocument.Command() + { + CurrentUser = currentUser, + BorrowId = borrowId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// Get all logs related to requests. /// diff --git a/src/Application/Borrows/Commands/ReportLostDocument.cs b/src/Application/Borrows/Commands/ReportLostDocument.cs new file mode 100644 index 00000000..30552316 --- /dev/null +++ b/src/Application/Borrows/Commands/ReportLostDocument.cs @@ -0,0 +1,98 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Commands; + +public class ReportLostDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid BorrowId { get; init; } + + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + + + + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + // Staff cant report lost documents from other department + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Borrowed) + { + throw new ConflictException("Document is not borrowed."); + } + + if (borrowRequest.Status is not (BorrowRequestStatus.Overdue or BorrowRequestStatus.CheckedOut)) + { + throw new ConflictException("Request cannot be lost."); + } + + // Get borrows for the lost document which is still pending + var borrowsForDocument = _context.Borrows + .Include(x => x.Document) + .Where( x => x.Id != request.BorrowId + && x.Document.Id == borrowRequest.Document.Id + && x.Status == BorrowRequestStatus.Pending); + + foreach (var borrow in borrowsForDocument) + { + borrow.Status = BorrowRequestStatus.NotProcessable; + } + + borrowRequest.Status = BorrowRequestStatus.Lost; + borrowRequest.Document.Status = DocumentStatus.Lost; + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs index 3d6a6387..fdf816d0 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -1,4 +1,4 @@ -using Application.Borrows.Commands; +using Application.Borrows.Commands; using Application.Common.Exceptions; using Application.Identity; using Domain.Entities; @@ -7,7 +7,6 @@ using FluentAssertions; using Infrastructure.Persistence; using Microsoft.Extensions.DependencyInjection; -using NodaTime; using Xunit; namespace Application.Tests.Integration.Borrows.Commands; @@ -16,7 +15,7 @@ public class BorrowDocumentTests : BaseClassFixture { public BorrowDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) { - + } [Fact] @@ -27,7 +26,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() var context = scope.ServiceProvider.GetRequiredService(); var department = CreateDepartment(); await context.AddAsync(department); - + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); user.Department = department; await context.AddAsync(user); @@ -46,10 +45,10 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() BorrowFrom = DateTime.Now.Add(TimeSpan.FromHours(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(1)), }; - + // Act var result = await SendAsync(command); - + // Assert result.DocumentId.Should().Be(command.DocumentId); result.BorrowerId.Should().Be(command.BorrowerId); @@ -57,7 +56,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() result.BorrowTime.Should().Be(command.BorrowFrom); result.DueTime.Should().Be(command.BorrowTo); result.Status.Should().Be(BorrowRequestStatus.Pending.ToString()); - + // Cleanup Remove(await FindAsync(result.Id)); Remove(user); @@ -72,7 +71,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() var document = CreateNDocuments(1).First(); document.Status = DocumentStatus.Available; await AddAsync(document); - + var command = new BorrowDocument.Command() { BorrowerId = Guid.NewGuid(), @@ -81,18 +80,18 @@ public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), BorrowReason = "Example", }; - + // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("User does not exist."); - + // Cleanup Remove(document); } - + [Fact] public async Task ShouldThrowConflictException_WhenUserIsNotActive() { @@ -100,11 +99,11 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActive() var document = CreateNDocuments(1).First(); document.Status = DocumentStatus.Available; await AddAsync(document); - + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); user.IsActive = false; await AddAsync(user); - + var command = new BorrowDocument.Command() { BorrowerId = user.Id, @@ -113,19 +112,19 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActive() BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), BorrowReason = "Example", }; - + // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("User is not active."); - + // Cleanup Remove(document); Remove(user); } - + [Fact] public async Task ShouldThrowConflictException_WhenUserIsNotActivated() { @@ -133,11 +132,11 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActivated() var document = CreateNDocuments(1).First(); document.Status = DocumentStatus.Available; await AddAsync(document); - + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); user.IsActivated = false; await AddAsync(user); - + var command = new BorrowDocument.Command() { BorrowerId = user.Id, @@ -146,14 +145,14 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActivated() BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), BorrowReason = "Example", }; - + // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("User is not activated."); - + // Cleanup Remove(document); Remove(user); @@ -177,15 +176,15 @@ public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("Document does not exist."); - + // Cleanup Remove(user); } - + [Fact] public async Task ShouldConflictException_WhenDocumentIsNotAvailable() { @@ -208,16 +207,16 @@ public async Task ShouldConflictException_WhenDocumentIsNotAvailable() // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("Document is not available."); - + // Cleanup Remove(user); Remove(document); } - + [Fact] public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheSameDepartment() { @@ -226,7 +225,7 @@ public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheS var context = scope.ServiceProvider.GetRequiredService(); var department1 = CreateDepartment(); var department2 = CreateDepartment(); - + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); user.Department = department1; await context.AddAsync(user); @@ -249,11 +248,11 @@ public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheS // Act var result = async () => await SendAsync(command); - + // Assert await result.Should().ThrowAsync() .WithMessage("User is not allowed to borrow this document."); - + // Cleanup Remove(user); Remove(document); From 229655ac7b699431db5a1bc3f21192a321c70396 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sat, 8 Jul 2023 11:35:06 +0700 Subject: [PATCH 088/162] feat: get entry by id (#327) * feat: get entry by id * i am stoopid * im stoopid 2 --- src/Api/Controllers/EntriesController.cs | 20 +++++++++ .../Digital/Queries/GetEntryById.cs | 44 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/Application/Digital/Queries/GetEntryById.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 3104f3d0..5996d20a 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -4,6 +4,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Digital; using Application.Digital.Commands; +using Application.Digital.Queries; using Application.Identity; using FluentValidation.Results; using Infrastructure.Identity.Authorization; @@ -89,6 +90,25 @@ public async Task>> UploadDigitalFile( /// /// /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{entryId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid entryId) + { + var query = new GetEntryById.Query() + { + EntryId = entryId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// + /// /// /// [RequiresRole(IdentityData.Roles.Employee)] diff --git a/src/Application/Digital/Queries/GetEntryById.cs b/src/Application/Digital/Queries/GetEntryById.cs new file mode 100644 index 00000000..8e0e1615 --- /dev/null +++ b/src/Application/Digital/Queries/GetEntryById.cs @@ -0,0 +1,44 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Digital.Queries; + +public class GetEntryById +{ + public record Query : IRequest + { + public Guid EntryId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.Uploader) + .ThenInclude(x => x.Department) + .Include(x => x.Owner) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + return _mapper.Map(entry); + } + } +} \ No newline at end of file From b85d5296d5e97554bd7a3345e5151df3d21bad23 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 8 Jul 2023 11:37:22 +0700 Subject: [PATCH 089/162] feat: get shared entry permission (#341) --- src/Api/Controllers/SharedController.cs | 19 +++++ .../Entries/Queries/GetPermissions.cs | 81 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/Application/Entries/Queries/GetPermissions.cs diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index bf0c347f..f63fdc92 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -40,4 +40,23 @@ public async Task>> DownloadSharedFile( var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } + + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId}/permissions")] + public async Task>> SharePermissions( + [FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetPermissions.Query + { + CurrentUser = currentUser, + EntryId = entryId + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetPermissions.cs b/src/Application/Entries/Queries/GetPermissions.cs new file mode 100644 index 00000000..75c663c4 --- /dev/null +++ b/src/Application/Entries/Queries/GetPermissions.cs @@ -0,0 +1,81 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class GetPermissions +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.File) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new ConflictException("Entry does not exist."); + } + + if (entry.OwnerId == request.CurrentUser.Id) + { + return CreateEntryPermissionDto(entry.Id, entry.OwnerId, + true, true, true, true, + false); + } + + var permission = await _context.EntryPermissions + .FirstOrDefaultAsync(x => + x.EntryId == entry.Id + && x.EmployeeId == request.CurrentUser.Id, cancellationToken); + + if (permission is null) + { + return CreateEntryPermissionDto(entry.Id, request.CurrentUser.Id, + false, false, false, false, + false); + } + + return _mapper.Map(permission); + } + + private static EntryPermissionDto CreateEntryPermissionDto( + Guid entryId, Guid userId, + bool canView, bool canUpload, bool canDownload, bool canChangePermission, + bool isSharedRoot) + { + return new EntryPermissionDto + { + EmployeeId = userId, + EntryId = entryId, + CanView = canView, + CanUpload = canUpload, + CanDownload = canDownload, + CanChangePermission = canChangePermission, + IsSharedRoot = isSharedRoot, + }; + } + } +} \ No newline at end of file From 9dc03b34e0a5e28855c1c38d1e766558705cacb0 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 9 Jul 2023 10:16:04 +0700 Subject: [PATCH 090/162] feat: get entries by entry path paginated (#331) --- src/Api/Controllers/EntriesController.cs | 19 +++++ .../GetAllEntriesPaginatedQueryParameters.cs | 9 +++ .../Digital/Queries/GetAllEntriesPaginated.cs | 72 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs create mode 100644 src/Application/Digital/Queries/GetAllEntriesPaginated.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 5996d20a..0922b9a9 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -89,6 +89,25 @@ public async Task>> UploadDigitalFile( /// /// /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllEntriesPaginatedQueryParameters queryParameters ) + { + var query = new GetAllEntriesPaginated.Query() + { + Page = queryParameters.Page, + Size = queryParameters.Size, + EntryPath = queryParameters.EntryPath, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder + }; + + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } /// /// [RequiresRole(IdentityData.Roles.Employee)] diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs new file mode 100644 index 00000000..873bd222 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.DigitalFile; + +/// +/// Get All Entries Paginated Query Parameters +/// +public class GetAllEntriesPaginatedQueryParameters : PaginatedQueryParameters +{ + public string EntryPath { get; set; } +} \ No newline at end of file diff --git a/src/Application/Digital/Queries/GetAllEntriesPaginated.cs b/src/Application/Digital/Queries/GetAllEntriesPaginated.cs new file mode 100644 index 00000000..39bf17a0 --- /dev/null +++ b/src/Application/Digital/Queries/GetAllEntriesPaginated.cs @@ -0,0 +1,72 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Digital.Queries; + +public class GetAllEntriesPaginated +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.EntryPath) + .NotEmpty().WithMessage("File's path is required.") + .Matches("^(/(?!/)[a-z_.\\-0-9]*)+(?> + { + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + public string EntryPath { get; init; } = null!; + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var entries = _context.Entries + .Where(x => x.Path.Equals(request.EntryPath)) + .AsQueryable(); + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(EntryDto.Path); + } + + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await entries.CountAsync(cancellationToken); + var list = await entries + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file From 07245b7b6b264592e9186ea623310dc2b0fadf57 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:43:45 +0700 Subject: [PATCH 091/162] implement getting shared users (#347) * feat: Get all shared users of a shared entry. * refactoring some logic * resolve some stuff * refactoring --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- ...sOfASharedEntryPaginatedQueryParameters.cs | 17 +++++ src/Api/Controllers/SharedController.cs | 26 ++++++- ...etAllSharedUsersOfASharedEntryPaginated.cs | 74 +++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs create mode 100644 src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs new file mode 100644 index 00000000..f2a15b7e --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs @@ -0,0 +1,17 @@ +namespace Api.Controllers.Payload.Requests.Users; + +public class GetAllSharedUsersFromASharedEntryPaginatedQueryParameters +{ + /// + /// Search term + /// + public string? SearchTerm { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index f63fdc92..9271156e 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -1,9 +1,11 @@ using Api.Controllers.Payload.Requests.Entries; +using Api.Controllers.Payload.Requests.Users; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; using Application.Entries.Queries; using Application.Identity; +using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -40,12 +42,34 @@ public async Task>> DownloadSharedFile( var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + /// /// /// /// [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId:guid}/shared-users")] + public async Task>> GetSharedUsersFromASharedEntryPaginated( + [FromRoute] Guid entryId, + [FromQuery] GetAllSharedUsersFromASharedEntryPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllSharedUsersOfASharedEntryPaginated.Query() + { + CurrentUser = currentUser, + Page = queryParameters.Page, + Size = queryParameters.Size, + SearchTerm = queryParameters.SearchTerm, + EntryId = entryId, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// + /// + /// [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId}/permissions")] public async Task>> SharePermissions( [FromRoute] Guid entryId) diff --git a/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs new file mode 100644 index 00000000..a8f02bd0 --- /dev/null +++ b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs @@ -0,0 +1,74 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Queries; + +public class GetAllSharedUsersOfASharedEntryPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class Handler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IMapper mapper, IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var permission = await _context.EntryPermissions + .FirstOrDefaultAsync(x => x.EntryId == request.EntryId + && x.EmployeeId == request.CurrentUser.Id, cancellationToken); + + if (permission is null || !permission.AllowedOperations.Contains(EntryOperation.View.ToString())) + { + throw new NotAllowedException("You do not have permission to view this shared entry's users."); + } + + var sharedUsers = _context.EntryPermissions + .Where(x => x.EntryId == request.EntryId) + .Select(x => x.Employee); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + sharedUsers = sharedUsers.Where(x => + x.Username.ToLower().Trim().Contains(request.SearchTerm.ToLower().Trim())); + } + + return await sharedUsers + .ListPaginateWithSortAsync( + request.Page, + request.Size, + null, + null, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file From 833d816dd16b98851c57d27ba0f068091ead6ad2 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:53:04 +0700 Subject: [PATCH 092/162] feat: add size to entry (#315) * feat: add size to entry * rename Size in Entry To SizeInBytes * remove magic number --- .../Common/Models/Dtos/Digital/EntryDto.cs | 2 +- .../Digital/Commands/UploadDigitalFile.cs | 5 +- src/Application/Helpers/FileUtil.cs | 7 + src/Domain/Entities/Digital/Entry.cs | 1 + .../Configurations/EntryConfiguration.cs | 3 + .../20230701174913_AddSizeEntry.Designer.cs | 1100 +++++++++++++++++ .../Migrations/20230701174913_AddSizeEntry.cs | 28 + ...20230708045158_renameSizeEntry.Designer.cs | 1100 +++++++++++++++++ .../20230708045158_renameSizeEntry.cs | 28 + .../ApplicationDbContextModelSnapshot.cs | 3 + 10 files changed, 2275 insertions(+), 2 deletions(-) create mode 100644 src/Application/Helpers/FileUtil.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.cs diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs index 7246ece6..29e5021f 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -13,7 +13,7 @@ public class EntryDto : BaseDto, IMapFrom public string? FileType { get; set; } public string? FileExtension { get; set; } public bool IsDirectory { get; set; } - + public long? SizeInBytes { get; set; } public UserDto Owner { get; set; } = null!; public DateTime Created { get; set; } public Guid? CreatedBy { get; set; } diff --git a/src/Application/Digital/Commands/UploadDigitalFile.cs b/src/Application/Digital/Commands/UploadDigitalFile.cs index c4ba035b..43a074d0 100644 --- a/src/Application/Digital/Commands/UploadDigitalFile.cs +++ b/src/Application/Digital/Commands/UploadDigitalFile.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Digital; +using Application.Helpers; using AutoMapper; using Domain.Entities; using Domain.Entities.Digital; @@ -74,6 +75,7 @@ public async Task Handle(Command request, CancellationToken cancellati Created = localDateTimeNow, Owner = request.CurrentUser, OwnerId = request.CurrentUser.Id, + SizeInBytes = null }; if (request.IsDirectory) @@ -91,7 +93,7 @@ public async Task Handle(Command request, CancellationToken cancellati else { // Make this dynamic - if (request.FileData!.Length > 20971520) + if (request.FileData!.Length > FileUtil.ToByteFromMb(20)) { throw new ConflictException("File size must be lower than 20MB"); } @@ -107,6 +109,7 @@ public async Task Handle(Command request, CancellationToken cancellati }; entryEntity.FileId = fileEntity.Id; entryEntity.File = fileEntity; + entryEntity.SizeInBytes = request.FileData!.Length; await _context.Files.AddAsync(fileEntity, cancellationToken); } diff --git a/src/Application/Helpers/FileUtil.cs b/src/Application/Helpers/FileUtil.cs new file mode 100644 index 00000000..c43e8887 --- /dev/null +++ b/src/Application/Helpers/FileUtil.cs @@ -0,0 +1,7 @@ +namespace Application.Helpers; + +public static class FileUtil +{ + public static long ToByteFromMb(this long sizeInMb) + => sizeInMb * 1024 * 1024; +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs index 7aa32206..0cf344d1 100644 --- a/src/Domain/Entities/Digital/Entry.cs +++ b/src/Domain/Entities/Digital/Entry.cs @@ -9,6 +9,7 @@ public class Entry : BaseAuditableEntity public string Path { get; set; } = null!; public Guid? FileId { get; set; } public Guid OwnerId { get; set; } + public long? SizeInBytes { get; set; } [NotMapped] public bool IsDirectory => FileId is null; public virtual FileEntity? File { get; set; } diff --git a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs index 01269855..a4c0a9c4 100644 --- a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs @@ -33,5 +33,8 @@ public void Configure(EntityTypeBuilder builder) .WithMany() .HasForeignKey(x => x.OwnerId) .IsRequired(); + + builder.Property(x => x.SizeInBytes) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.Designer.cs new file mode 100644 index 00000000..0433043c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.Designer.cs @@ -0,0 +1,1100 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230701174913_AddSizeEntry")] + partial class AddSizeEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.cs b/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.cs new file mode 100644 index 00000000..459b3a39 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230701174913_AddSizeEntry.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddSizeEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Size", + table: "Entries", + type: "bigint", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Size", + table: "Entries"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.Designer.cs new file mode 100644 index 00000000..df612dbb --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.Designer.cs @@ -0,0 +1,1100 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230708045158_renameSizeEntry")] + partial class renameSizeEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeInBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.cs b/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.cs new file mode 100644 index 00000000..9d233696 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230708045158_renameSizeEntry.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class renameSizeEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Size", + table: "Entries", + newName: "SizeInBytes"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "SizeInBytes", + table: "Entries", + newName: "Size"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index c15a342e..eab106c3 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -112,6 +112,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("SizeInBytes") + .HasColumnType("bigint"); + b.HasKey("Id"); b.HasIndex("CreatedBy"); From 4bdd25dd9c00f9bee1f9612c05da9a776b50949b Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:53:51 +0700 Subject: [PATCH 093/162] fix: made description not required when adding a room. (#277) --- src/Application/Rooms/Commands/AddRoom.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index 00bf55c0..c17a3bd0 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -36,7 +36,6 @@ public Validator(IApplicationDbContext context) .Must(BeUnique).WithMessage("Room name already exists."); RuleFor(x => x.Description) - .NotEmpty().WithMessage("Description is required.") .MaximumLength(256).WithMessage("Description cannot exceed 256 characters."); } @@ -48,7 +47,7 @@ private bool BeUnique(string name) public record Command : IRequest { - public User CurrentUser { get; init; } + public User CurrentUser { get; init; } = null!; public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } From bdb766328765b05d93f21761833aaade0425e528 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:55:05 +0700 Subject: [PATCH 094/162] feat: download a shared entry (#345) --- src/Api/Controllers/SharedController.cs | 23 +++++- .../Entries/Queries/DownloadSharedEntry.cs | 71 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Application/Entries/Queries/DownloadSharedEntry.cs diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 9271156e..9d635e24 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -20,13 +20,34 @@ public SharedController(ICurrentUserService currentUserService) _currentUserService = currentUserService; } + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId:guid}/file")] + public async Task DownloadSharedFile([FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new DownloadSharedEntry.Query() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + var result = await Mediator.Send(query); + var content = new MemoryStream(result.FileData); + HttpContext.Response.ContentType = result.FileType; + return File(content, result.FileType, result.FileName); + } + /// /// /// /// [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries")] - public async Task>> DownloadSharedFile( + public async Task>> GetAll( [FromQuery] GetAllEntriesPaginatedQueryParameters queryParameters) { var currentUser = _currentUserService.GetCurrentUser(); diff --git a/src/Application/Entries/Queries/DownloadSharedEntry.cs b/src/Application/Entries/Queries/DownloadSharedEntry.cs new file mode 100644 index 00000000..2b9bf3bd --- /dev/null +++ b/src/Application/Entries/Queries/DownloadSharedEntry.cs @@ -0,0 +1,71 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Operations; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class DownloadSharedEntry +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Result + { + public string FileName { get; set; } = null!; + public string FileType { get; set; } = null!; + public byte[] FileData { get; set; } = null!; + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries.FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + if (entry.IsDirectory) + { + throw new ConflictException("Entry cannot be downloaded."); + } + + if (entry.OwnerId != request.CurrentUser.Id) + { + var permission = await _context.EntryPermissions.FirstOrDefaultAsync( + x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, cancellationToken); + + if (permission is null || + !permission.AllowedOperations + .Split(",") + .Contains(EntryOperation.Download.ToString())) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + + var result = new Result + { + FileName = $"{entry.Name}.{entry.File!.FileExtension}", + FileType = entry.File.FileType, + FileData = entry.File.FileData, + }; + + return result; + } + } +} \ No newline at end of file From be2b50839f77468174d85512f5f97ac2ad736f6f Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:11:26 +0700 Subject: [PATCH 095/162] feat: implement deleting entry in bin (#325) * feat: implement deleting entry in bin * Update DeleteEntry.cs * refactoring --- src/Api/Controllers/BinController.cs | 41 ++++++++++ src/Api/Middlewares/ExceptionMiddleware.cs | 6 ++ .../Common/Exceptions/NotChangedException.cs | 8 ++ .../Digital/Commands/DeleteBinEntry.cs | 78 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 src/Api/Controllers/BinController.cs create mode 100644 src/Application/Common/Exceptions/NotChangedException.cs create mode 100644 src/Application/Digital/Commands/DeleteBinEntry.cs diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs new file mode 100644 index 00000000..b12b9a93 --- /dev/null +++ b/src/Api/Controllers/BinController.cs @@ -0,0 +1,41 @@ +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using Application.Digital.Commands; +using Application.Identity; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class BinController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + + public BinController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpDelete("entries/{entryId:guid}")] + public async Task>> DeleteBinEntry( + [FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + + var command = new DeleteBinEntry.Command() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index 3a52aae5..a74f0d5c 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -35,6 +35,7 @@ public ExceptionMiddleware() { typeof(InvalidOperationException), HandleInvalidOperationException }, { typeof(AuthenticationException), HandleAuthenticationException }, { typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException }, + { typeof(NotChangedException), HandleNotChangedException }, }; } @@ -110,6 +111,11 @@ private static async void HandleInvalidOperationException(HttpContext context, E await WriteExceptionMessageAsync(context, ex); } + private static async void HandleNotChangedException(HttpContext context, Exception ex) + { + context.Response.StatusCode = StatusCodes.Status204NoContent; + } + private static async Task WriteExceptionMessageAsync(HttpContext context, Exception ex) { await context.Response.Body.WriteAsync(SerializeToUtf8BytesWeb(Result.Fail(ex))); diff --git a/src/Application/Common/Exceptions/NotChangedException.cs b/src/Application/Common/Exceptions/NotChangedException.cs new file mode 100644 index 00000000..3ba19843 --- /dev/null +++ b/src/Application/Common/Exceptions/NotChangedException.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Exceptions; + +public class NotChangedException : Exception +{ + public NotChangedException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/Application/Digital/Commands/DeleteBinEntry.cs b/src/Application/Digital/Commands/DeleteBinEntry.cs new file mode 100644 index 00000000..3012351e --- /dev/null +++ b/src/Application/Digital/Commands/DeleteBinEntry.cs @@ -0,0 +1,78 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class DeleteBinEntry +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + private const string BinString = "_bin"; + + public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider) + { + _mapper = mapper; + _context = context; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x=> x.Owner) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var entryPath = entry.Path; + var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); + var binCheck = entryPath.Substring(0, firstSlashIndex); + + if (!binCheck.Contains(BinString)) + { + throw new NotChangedException("Entry is not in bin."); + } + + if (!entry.Owner.Username.Equals(request.CurrentUser.Username)) + { + throw new NotAllowedException("You do not have the permission to delete this entry."); + } + + if (entry.IsDirectory) + { + var path = entry.Path[^1].Equals('/') ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var pattern = $"{path}/%"; + var childEntries = _context.Entries.Where(x => + x.Path.Trim().Equals(path) + || EF.Functions.Like(x.Path, pattern)); + + foreach (var childEntry in childEntries) + { + _context.Entries.Remove(childEntry); + } + } + + var result = _context.Entries.Remove(entry); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} From e3a6c923475961a2920b0588cfc30d97d7787189 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:22:04 +0700 Subject: [PATCH 096/162] update: added role checking when adding user (#278) --- src/Application/Users/Commands/AddUser.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index a9c0fd7a..52c07f0e 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -112,6 +112,12 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Department does not exist."); } + if (!request.Role.Trim().Equals(IdentityData.Roles.Staff) && + !request.Role.Trim().Equals(IdentityData.Roles.Employee)) + { + throw new ConflictException("Invalid role provided."); + } + var password = StringUtil.RandomPassword(); var salt = StringUtil.RandomSalt(); From 42478b9528867a8cb51680a46d106cc7cd0184ef Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 9 Jul 2023 18:29:39 +0700 Subject: [PATCH 097/162] Feat : Upload file or create directory on shared entry by entry ID (#343) * feat: implementation of add file/directpry to shared entry * feat: finish ta da --- .../DigitalFile/UploadSharedEntryRequest.cs | 11 ++ src/Api/Controllers/SharedController.cs | 77 ++++++++++- .../Digital/Commands/UploadSharedEntry.cs | 130 ++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs create mode 100644 src/Application/Digital/Commands/UploadSharedEntry.cs diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs b/src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs new file mode 100644 index 00000000..ab02aeed --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs @@ -0,0 +1,11 @@ +namespace Api.Controllers.Payload.Requests.DigitalFile; + +/// +/// +/// +public class UploadSharedEntryRequest +{ + public string Name { get; set; } = null!; + public bool IsDirectory { get; set; } + public IFormFile? File { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 9d635e24..525c4670 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -1,10 +1,14 @@ +using Api.Controllers.Payload.Requests.DigitalFile; using Api.Controllers.Payload.Requests.Entries; +using Application.Common.Exceptions; using Api.Controllers.Payload.Requests.Users; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; +using Application.Digital.Commands; using Application.Entries.Queries; using Application.Identity; +using FluentValidation.Results; using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -63,6 +67,76 @@ public async Task>> GetAll( var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } + + + /// + /// upload a file or create a directory to a shared entry + /// + /// an EntryDto + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("entries/{entryId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> UploadSharedEntry([ + FromRoute] Guid entryId, + [FromForm] UploadSharedEntryRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + if (request is { IsDirectory: true, File: not null }) + { + throw new RequestValidationException(new List() + { + new("File", "Cannot create a directory with a file.") + }); + } + + if (request is { IsDirectory: false, File: null }) + { + throw new RequestValidationException(new List() + { + new("File", "Cannot upload with no files.") + }); + } + + UploadSharedEntry.Command command; + + if (request.IsDirectory) + { + command = new UploadSharedEntry.Command() + { + Name = request.Name, + CurrentUser = currentUser, + EntryId = entryId, + FileData = null, + FileExtension = null, + FileType = null, + IsDirectory = true + }; + } + else + { + var fileData = new MemoryStream(); + await request.File!.CopyToAsync(fileData); + var lastDotIndex = request.File.FileName.LastIndexOf(".", StringComparison.Ordinal); + var extension = + request.File.FileName.Substring(lastDotIndex + 1, request.File.FileName.Length - lastDotIndex - 1); + command = new UploadSharedEntry.Command() + { + CurrentUser = currentUser, + EntryId = entryId, + Name = request.Name, + IsDirectory = false, + FileType = request.File.ContentType, + FileData = fileData, + FileExtension = extension, + }; + } + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} /// /// @@ -104,4 +178,5 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } -} \ No newline at end of file +} + diff --git a/src/Application/Digital/Commands/UploadSharedEntry.cs b/src/Application/Digital/Commands/UploadSharedEntry.cs new file mode 100644 index 00000000..e3aab599 --- /dev/null +++ b/src/Application/Digital/Commands/UploadSharedEntry.cs @@ -0,0 +1,130 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class UploadSharedEntry +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); + } + } + public record Command : IRequest + { + public Guid EntryId { get; init; } + public User CurrentUser { get; init; } = null!; + public string Name { get; init; } = null!; + public bool IsDirectory { get; init; } + public MemoryStream? FileData { get; init; } + public string? FileType { get; init; } + public string? FileExtension { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var permissions = _context.EntryPermissions + .Include(x => x.Entry) + .ThenInclude(y => y.Uploader) + .Include(x => x.Entry) + .ThenInclude(x => x.Owner) + .Where(x => x.EmployeeId == request.CurrentUser.Id + && x.AllowedOperations.Contains(EntryOperation.Upload.ToString())); + + var rootEntry = await permissions + .Select(x => x.Entry) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (rootEntry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + if (!rootEntry.IsDirectory) + { + throw new ConflictException("This is a file."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entryPath = rootEntry.Path + "/" + request.Name.Trim(); + + var entity = new Entry() + { + Name = request.Name.Trim(), + Path = entryPath, + CreatedBy = request.CurrentUser.Id, + Uploader = request.CurrentUser, + Created = localDateTimeNow, + Owner = rootEntry.Owner, + OwnerId = rootEntry.Owner.Id, + }; + + if (request.IsDirectory) + { + var entry = await _context.Entries + .FirstOrDefaultAsync( + x => x.Name.Trim().Equals(request.Name.Trim()) + && x.Path.Equals(entryPath) + && x.FileId == null,cancellationToken); + + if (entry is not null) + { + throw new ConflictException("Directory already exists."); + } + } + else + { + if (request.FileData!.Length > 20971520) + { + throw new ConflictException("File size must be lower than 20MB"); + } + + var lastDotIndex = request.Name.LastIndexOf(".", StringComparison.Ordinal); + var fileExtension = request.Name.Substring(lastDotIndex + 1, request.Name.Length - lastDotIndex - 1); + + var fileEntity = new FileEntity() + { + FileData = request.FileData.ToArray(), + FileType = request.FileType!, + FileExtension = request.FileExtension, + }; + + await _context.Files.AddAsync(fileEntity, cancellationToken); + + entity.FileId = fileEntity.Id; + entity.File = fileEntity; + } + + var result = await _context.Entries.AddAsync(entity, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From 6133db1c51ee2fa4e970bb2c151197673e5fae04 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 9 Jul 2023 18:50:21 +0700 Subject: [PATCH 098/162] i am stoopid stoopid (#350) --- src/Api/Controllers/SharedController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 525c4670..befe7484 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -12,6 +12,7 @@ using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; +using GetAllEntriesPaginatedQueryParameters = Api.Controllers.Payload.Requests.Entries.GetAllEntriesPaginatedQueryParameters; namespace Api.Controllers; @@ -136,8 +137,7 @@ public async Task>> UploadSharedEntry([ var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } -} - + /// /// /// From ec7fddf6f3f85a6105abe9c6795265007daabd9b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Mon, 10 Jul 2023 00:56:42 +0700 Subject: [PATCH 099/162] fix: a couple things (#284) - GetAllImportRequestsPaginated.cs now accept statuses parameter, which is an array of literals and case-insensitive like issued, rEjEcTED, and filter it out by the ones that is valid and convertable - Fix an authorization logic in GetAllImportRequestsPaginated.cs that when a user is an Employee and he tries to use this endpoint, he does not have to provide a roomId, now if he does not, he will be returned back a list of import requests that is of his Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> --- .../Controllers/ImportRequestsController.cs | 1 + ...lImportRequestsPaginatedQueryParameters.cs | 1 + .../Queries/GetAllImportRequestsPaginated.cs | 51 ++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs index dc7f0bfc..d2875e22 100644 --- a/src/Api/Controllers/ImportRequestsController.cs +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -65,6 +65,7 @@ public async Task>>> GetAllI { CurrentUser = currentUser, SearchTerm = queryParameters.SearchTerm, + Statuses = queryParameters.Statuses, RoomId = queryParameters.RoomId, Page = queryParameters.Page, Size = queryParameters.Size, diff --git a/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs index c66c1956..dfb22205 100644 --- a/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs @@ -7,4 +7,5 @@ public class GetAllImportRequestsPaginatedQueryParameters : PaginatedQueryParame { public string? SearchTerm { get; set; } public Guid? RoomId { get; set; } + public string[]? Statuses { get; set; } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs index f90d4991..6719e114 100644 --- a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs +++ b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs @@ -5,6 +5,7 @@ using AutoMapper; using Domain.Entities; using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -16,6 +17,7 @@ public record Query : IRequest> { public User CurrentUser { get; init; } = null!; public Guid? RoomId { get; init; } + public string[]? Statuses { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -54,34 +56,46 @@ public async Task> Handle(Query request, Cancell } } - if (request.CurrentUser.Role.IsEmployee()) - { - if (request.RoomId is null) - { - throw new UnauthorizedAccessException("User can not access this resource."); - } - - var room = await _context.Rooms - .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - var roomDoesNotExist = room is null; - - if (roomDoesNotExist - || RoomIsNotInSameDepartment(request.CurrentUser, room!)) - { - throw new UnauthorizedAccessException("User can not access this resource."); - } - } - var importRequests = _context.ImportRequests .Include(x => x.Document) .Include(x => x.Room) .ThenInclude(x => x.Department) .AsQueryable(); + + if (request.CurrentUser.Role.IsEmployee()) + { + importRequests = importRequests.Where(x => x.CreatedBy! == request.CurrentUser.Id); + if (request.RoomId is not null) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + } if (request.RoomId is not null) { importRequests = importRequests.Where(x => x.RoomId == request.RoomId); } + + if (request.Statuses is not null) + { + var statuses = request.Statuses.Aggregate(new List(), + (statuses, currentStatus) => + { + if (!Enum.TryParse(currentStatus, true, out var validStatus)) return statuses; + + statuses.Add(validStatus); + return statuses; + }); + importRequests = importRequests.Where(x => statuses.Contains(x.Status)); + } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { @@ -89,7 +103,6 @@ public async Task> Handle(Query request, Cancell x.Document.Title.ToLower().Contains(request.SearchTerm.ToLower())); } - return await importRequests .ListPaginateWithSortAsync( request.Page, From 7c4037447c008399f31f86cb68fac6a409929dcf Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 01:02:52 +0700 Subject: [PATCH 100/162] fix: get rooms by department id (#286) Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> --- src/Api/Controllers/DepartmentsController.cs | 8 +++- src/Application/Common/Models/ItemResult.cs | 11 ++++++ .../Rooms/Queries/GetRoomByDepartmentId.cs | 39 +++++++++++++------ 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 src/Application/Common/Models/ItemResult.cs diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 0b425045..37716a83 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -60,15 +60,19 @@ public async Task>> GetById( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetRoomByDepartmentId( + public async Task>>> GetRoomByDepartmentId( [FromRoute] Guid departmentId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetRoomByDepartmentId.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, DepartmentId = departmentId, }; var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); + return Ok(Result>.Succeed(result)); } /// diff --git a/src/Application/Common/Models/ItemResult.cs b/src/Application/Common/Models/ItemResult.cs new file mode 100644 index 00000000..0b04fa49 --- /dev/null +++ b/src/Application/Common/Models/ItemResult.cs @@ -0,0 +1,11 @@ +namespace Application.Common.Models; + + +public class ItemsResult +{ + public ItemsResult(IEnumerable items) + { + Items = items; + } + public IEnumerable Items { get; } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs index a301bc3f..7a3aa1de 100644 --- a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs +++ b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; @@ -10,12 +12,14 @@ namespace Application.Rooms.Queries; public class GetRoomByDepartmentId { - public record Query : IRequest + public record Query : IRequest> { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid DepartmentId { get; init; } } - public class QueryHandler : IRequestHandler + public class QueryHandler : IRequestHandler> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -26,19 +30,32 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task Handle(Query request, CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { - var room = await _context.Rooms - .Include(x => x.Department) - .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.DepartmentId == request.DepartmentId, cancellationToken); - - if (room is null) + + if ((request.CurrentUserRole.IsEmployee() + || request.CurrentUserRole.IsStaff()) + && request.CurrentUserDepartmentId != request.DepartmentId) { - throw new KeyNotFoundException("Room does not exist."); + throw new UnauthorizedAccessException("User cannot access this resource."); } + + var department = await _context.Departments + .FirstOrDefaultAsync(x => x.Id == request.DepartmentId, cancellationToken); + + if (department is null) + { + throw new KeyNotFoundException("Department does not exist."); + } + + var rooms = _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) + .Where(x => x.DepartmentId == request.DepartmentId); - return _mapper.Map(room); + var result = new ItemsResult + (new ReadOnlyCollection(_mapper.Map>(rooms))); + return result; } } } \ No newline at end of file From 1b4cae80b0826fe00d59520302ca001fb350a06e Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 01:03:52 +0700 Subject: [PATCH 101/162] feat: download digital file (#329) * feat: download digital file * fix: resolve stuff * feat: add check for permission * stoopid me --- src/Api/Controllers/EntriesController.cs | 26 ++++++ .../Digital/Commands/DownloadDigitalFile.cs | 81 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/Application/Digital/Commands/DownloadDigitalFile.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 0922b9a9..314ab0b7 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -151,4 +151,30 @@ public async Task>> ManagePermission( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Download a file + /// + /// + /// The download file + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{entryId:guid}/file")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DownloadFile([FromRoute] Guid entryId) + { + var currentUserId = _currentUserService.GetId(); + var command = new DownloadDigitalFile.Command() + { + CurrentUserId = currentUserId, + EntryId = entryId + }; + + var result = await Mediator.Send(command); + HttpContext.Response.ContentType = result.FileType; + return File(result.Content, result.FileType, result.FileName); + } + } \ No newline at end of file diff --git a/src/Application/Digital/Commands/DownloadDigitalFile.cs b/src/Application/Digital/Commands/DownloadDigitalFile.cs new file mode 100644 index 00000000..1525af0e --- /dev/null +++ b/src/Application/Digital/Commands/DownloadDigitalFile.cs @@ -0,0 +1,81 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Digital.Commands; + +public class DownloadDigitalFile +{ + public record Command : IRequest + { + public Guid CurrentUserId { get; init;} + public Guid EntryId { get; init; } + } + + public class Result + { + public MemoryStream Content { get; set; } = null!; + public string FileType { get; set; } = null!; + public string FileName { get; set; } = null!; + } + + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public CommandHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.File) + .Include(x => x.Owner) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + if (entry.IsDirectory) + { + throw new ConflictException("Can not download folder."); + } + + if (entry.OwnerId != request.CurrentUserId) + { + var permission = await _context.EntryPermissions.FirstOrDefaultAsync( + x => x.EntryId == request.EntryId + && x.EmployeeId == request.CurrentUserId, cancellationToken); + + if (permission is null || + !permission.AllowedOperations + .Split(",") + .Contains(EntryOperation.Download.ToString())) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + + var content = new MemoryStream(entry.File!.FileData); + var fileType = entry.File!.FileType; + var fileExtension = entry.File!.FileExtension; + + return new Result() + { + Content = content, + FileType = fileType, + FileName = $"{entry.Name}.{fileExtension}", + }; + } + } +} \ No newline at end of file From 6ab6092c161ca16d8112be6b72a0aedf2cb7bd68 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Mon, 10 Jul 2023 01:04:01 +0700 Subject: [PATCH 102/162] fix: update logic when an employee get physical resources (#309) * fix: update logic when an get physical resources * fix: my stupid checks --- .../Queries/GetAllBorrowRequestsPaginated.cs | 45 +++++++++---------- .../Documents/Queries/GetDocumentById.cs | 30 ++++++++----- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index ee822637..3d378831 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -58,29 +58,6 @@ public async Task> Handle(Query request, } } - if (request.CurrentUser.Role.IsEmployee()) - { - if (request.RoomId is null) - { - throw new UnauthorizedAccessException("User can not access this resource."); - } - - if (request.EmployeeId != request.CurrentUser.Id) - { - throw new UnauthorizedAccessException("User can not access this resource"); - } - - var room = await _context.Rooms - .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - var roomDoesNotExist = room is null; - - if (roomDoesNotExist - || RoomIsNotInSameDepartment(request.CurrentUser, room!)) - { - throw new UnauthorizedAccessException("User can not access this resource."); - } - } - var borrows = _context.Borrows.AsQueryable(); borrows = borrows @@ -93,6 +70,28 @@ public async Task> Handle(Query request, .ThenInclude(t => t.Room) .ThenInclude(s => s.Department); + if (request.CurrentUser.Role.IsEmployee()) + { + + if (request.EmployeeId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("User can not access this resource"); + } + + if (request.RoomId is not null) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + } + if (request.RoomId is not null) { borrows = borrows.Where(x => x.Document.Folder!.Locker.Room.Id == request.RoomId); diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index 9babe0a2..e8d35247 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -50,26 +50,34 @@ public async Task Handle(Query request, CancellationToken cancellat { throw new KeyNotFoundException("Document does not exist."); } - - if (ViolateConstraints(request.CurrentUser, document)) - { + + if (ViolateConstraintForStaffAndEmployee(request.CurrentUser, document)) { throw new UnauthorizedAccessException("You don't have permission to view this document."); } - + return _mapper.Map(document); } - private bool ViolateConstraints(User user, Document document) - => IsStaffAndNotInSameDepartment(user, document) - || IsEmployeeAndDoesNotHasReadPermission(user, document); - - private static bool IsStaffAndNotInSameDepartment(User user, Document document) - => user.Role.IsStaff() - && user.Department!.Id != document.Department!.Id; + private bool ViolateConstraintForStaffAndEmployee(User user, Document document) + => !IsInSameDepartment(user, document) + && document.IsPrivate + && ( IsStaffAndNotInSameRoom(user, document) + || IsEmployeeAndDoesNotHasReadPermission(user, document) ); + + private bool IsStaffAndNotInSameRoom(User user, Document document) + { + var staff = _context.Staffs.FirstOrDefault(x => x.Id == user.Id); + return user.Role.IsStaff() + && staff is not null + && !staff.Room!.Id.Equals(document.Folder?.Locker.Room.Id); + } private bool IsEmployeeAndDoesNotHasReadPermission(User user, Document document) => user.Role.IsEmployee() && document.ImporterId != user.Id && !_permissionManager.IsGranted(document.Id, DocumentOperation.Read, user.Id); + + private bool IsInSameDepartment(User user, Document document) + => user.Department == document.Department; } } \ No newline at end of file From dd57e7f208ccb7e5c959aaa7df0176962e296298 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 01:10:56 +0700 Subject: [PATCH 103/162] feat: update entry (#332) * feat: update entry * im stoopid * im stoopid 2 * i alone * resolve minor error --- src/Api/Controllers/EntriesController.cs | 25 ++++++ .../Requests/Entries/UpdateEntryRequest.cs | 6 ++ .../Digital/Commands/UpdateEntry.cs | 83 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/Entries/UpdateEntryRequest.cs create mode 100644 src/Application/Digital/Commands/UpdateEntry.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 314ab0b7..0a9ebcab 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.DigitalFile; +using Api.Controllers.Payload.Requests.Entries; using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; @@ -85,6 +86,30 @@ public async Task>> UploadDigitalFile( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPut("{entryId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + + public async Task>> Update( + [FromRoute] Guid entryId, + [FromBody] UpdateEntryRequest request) + { + var currentUserId = _currentUserService.GetId(); + var command = new UpdateEntry.Command() + { + Name = request.Name, + EntryId = entryId, + CurrentUserId = currentUserId + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// diff --git a/src/Api/Controllers/Payload/Requests/Entries/UpdateEntryRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/UpdateEntryRequest.cs new file mode 100644 index 00000000..d1fb81f8 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Entries/UpdateEntryRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Entries; + +public class UpdateEntryRequest +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Application/Digital/Commands/UpdateEntry.cs b/src/Application/Digital/Commands/UpdateEntry.cs new file mode 100644 index 00000000..b456342f --- /dev/null +++ b/src/Application/Digital/Commands/UpdateEntry.cs @@ -0,0 +1,83 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class UpdateEntry +{ + public record Command : IRequest + { + public Guid CurrentUserId { get; init; } + public Guid EntryId { get; init; } + public string Name { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.Owner) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + if (entry.OwnerId != request.CurrentUserId) + { + var permission = await _context.EntryPermissions.FirstOrDefaultAsync( + x => x.EntryId == request.EntryId + && x.EmployeeId == request.CurrentUserId, cancellationToken); + + if (permission is null || + !permission.AllowedOperations + .Split(",") + .Contains(EntryOperation.Upload.ToString())) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + + var nameExisted = await _context.Entries + .AnyAsync(x => x.Id != entry.Id + && x.Path.Equals(entry.Path) + && x.Name.Equals(request.Name), cancellationToken); + + if (nameExisted) + { + throw new ConflictException("Name has already exist."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + entry.Name = request.Name; + entry.LastModified = localDateTimeNow; + entry.LastModifiedBy = request.CurrentUserId; + + var result = _context.Entries.Update(entry); + await _context.SaveChangesAsync(cancellationToken); + + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From 6f3bb62046cd8e425ae8ae63109c87d617a190b0 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 08:40:36 +0700 Subject: [PATCH 104/162] hotfix: cuu-dev (#351) --- src/Api/Controllers/EntriesController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 0a9ebcab..1dcc63f4 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -10,6 +10,7 @@ using FluentValidation.Results; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; +using GetAllEntriesPaginatedQueryParameters = Api.Controllers.Payload.Requests.DigitalFile.GetAllEntriesPaginatedQueryParameters; namespace Api.Controllers; From 7e0b86e91a20c5d86aacfe9abea80c469fe644c0 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:40:23 +0700 Subject: [PATCH 105/162] update: a couple things (#287) - modify the statuses filter param of GetAllBorrowRequestsPaginated.cs Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> --- src/Api/Controllers/BorrowsController.cs | 1 + ...lBorrowRequestsPaginatedQueryParameters.cs | 1 + .../Queries/GetAllBorrowRequestsPaginated.cs | 22 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 4bc73e6c..0503bb34 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -91,6 +91,7 @@ public async Task>>> GetAllRequests RoomId = queryParameters.RoomId, EmployeeId = queryParameters.EmployeeId, DocumentId = queryParameters.DocumentId, + Statuses = queryParameters.Statuses, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs index aa844929..7fc42536 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs @@ -11,4 +11,5 @@ public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParame public Guid? RoomId { get; set; } public Guid? DocumentId { get; set; } public Guid? EmployeeId { get; set; } + public string[]? Statuses { get; init; } } \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index 3d378831..b691e0ee 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -23,7 +23,7 @@ public record Query : IRequest> public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } - public string? Status { get; init; } + public string[]? Statuses { get; init; } } public class QueryHandler : IRequestHandler> @@ -107,19 +107,17 @@ public async Task> Handle(Query request, borrows = borrows.Where(x => x.Document.Id == request.DocumentId); } - if (request.Status is not null) + if (request.Statuses is not null) { - var statuses = request.Status.Split(","); - var enums = new List(); - foreach (var status in statuses) - { - if (Enum.TryParse(status, true, out BorrowRequestStatus s)) + var statuses = request.Statuses.Aggregate(new List(), + (statuses, currentStatus) => { - enums.Add(s); - } - } - - borrows = borrows.Where(x => enums.Contains(x.Status)); + if (!Enum.TryParse(currentStatus, true, out var validStatus)) return statuses; + + statuses.Add(validStatus); + return statuses; + }); + borrows = borrows.Where(x => statuses.Contains(x.Status)); } var sortBy = request.SortBy; From e190c7233f5cbffd608d968c51e8b98594dae87e Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:47:57 +0700 Subject: [PATCH 106/162] implement getting shared entry by id (#349) * feat: get shared entry by id. * refactoring --------- Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> --- src/Api/Controllers/SharedController.cs | 22 +++++++- .../Entries/Queries/GetSharedEntryById.cs | 54 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/Application/Entries/Queries/GetSharedEntryById.cs diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index befe7484..9290917d 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -178,5 +178,23 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } -} - + + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId:guid}")] + public async Task>> GetSharedEntryById( + [FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetSharedEntryById.Query() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetSharedEntryById.cs b/src/Application/Entries/Queries/GetSharedEntryById.cs new file mode 100644 index 00000000..33e0fc17 --- /dev/null +++ b/src/Application/Entries/Queries/GetSharedEntryById.cs @@ -0,0 +1,54 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class GetSharedEntryById +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IMapper mapper, IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var permission = await _context.EntryPermissions + .Include(x => x.Entry) + .ThenInclude(x => x.Uploader) + .Include(x => x.Entry) + .ThenInclude(x => x.Owner) + .FirstOrDefaultAsync(x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, + cancellationToken); + + if (permission is null) + { + throw new KeyNotFoundException("Shared entry does not exist."); + } + + if (!permission.AllowedOperations.Contains(EntryOperation.View.ToString())) + { + throw new NotAllowedException("You do not have permission to view this shared entry."); + } + + return _mapper.Map(permission.Entry); + } + } +} \ No newline at end of file From c98c00edf635f246c9186ae339c96a18b6ed6420 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 20:49:26 +0700 Subject: [PATCH 107/162] fix: add IsAvailable to update (#353) * fix: add IsAvailable to update * author --- src/Api/Controllers/FoldersController.cs | 2 ++ src/Api/Controllers/LockersController.cs | 1 + .../Payload/Requests/Folders/UpdateFolderRequest.cs | 4 ++++ .../Payload/Requests/Lockers/UpdateLockerRequest.cs | 4 ++++ src/Application/Folders/Commands/UpdateFolder.cs | 2 ++ src/Application/Lockers/Commands/UpdateLocker.cs | 2 ++ 6 files changed, 15 insertions(+) diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 5114d8e1..4ec4d2e5 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -139,6 +139,7 @@ public async Task>> RemoveFolder([FromRoute] Guid /// Id of the folder to be updated /// Update folder details /// A FolderDto of the updated folder + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("{folderId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -158,6 +159,7 @@ public async Task>> Update( Name = request.Name, Description = request.Description, Capacity = request.Capacity, + IsAvailable = request.IsAvailable, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index c48dcc9a..ea9f9ef1 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -153,6 +153,7 @@ public async Task>> Update( Name = request.Name, Description = request.Description, Capacity = request.Capacity, + IsAvailable = request.IsAvailable, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs index e26cf33b..b62943b2 100644 --- a/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs @@ -17,4 +17,8 @@ public class UpdateFolderRequest /// New capacity of the folder to be updated /// public int Capacity { get; set; } + /// + /// Status of folder to be updated + /// + public bool IsAvailable { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs index 5558c1ce..a6daa0aa 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs @@ -17,4 +17,8 @@ public class UpdateLockerRequest /// New capacity of the locker to be updated /// public int Capacity { get; set; } + /// + /// Status of locker to be updated + /// + public bool IsAvailable { get; set; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index 3abe34fa..b61efac9 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -45,6 +45,7 @@ public record Command : IRequest public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } + public bool IsAvailable { get; init; } } public class CommandHandler : IRequestHandler @@ -98,6 +99,7 @@ public async Task Handle(Command request, CancellationToken cancellat folder.Capacity = request.Capacity; folder.LastModified = localDateTimeNow; folder.LastModifiedBy = request.CurrentUser.Id; + folder.IsAvailable = request.IsAvailable; var log = new FolderLog() { diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 49f1ad55..9826b345 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -42,6 +42,7 @@ public record Command : IRequest public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } + public bool IsAvailable { get; init; } } public class CommandHandler : IRequestHandler @@ -88,6 +89,7 @@ public async Task Handle(Command request, CancellationToken cancellat locker.Capacity = request.Capacity; locker.LastModified = localDateTimeNow; locker.LastModifiedBy = request.CurrentUser.Id; + locker.IsAvailable = request.IsAvailable; From 179b350828fdd91d085663daddd8f87cb45223ae Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:28:24 +0700 Subject: [PATCH 108/162] feat: implement restoring entry (#322) * feat: implement restoring entry * fix: constraints and added restoring child entries, added a new exception * Update ExceptionMiddleware.cs * Update RestoreEntry.cs * refactoring * updated restore business logic * refactor * Update RestoreBinEntry.cs * fix: remove duplicated ExceptionHandler --------- Co-authored-by: kaitoz11 <43519768+kaitoz11@users.noreply.github.com> --- src/Api/Controllers/BinController.cs | 22 +++ src/Api/Middlewares/ExceptionMiddleware.cs | 4 +- .../Digital/Commands/RestoreBinEntry.cs | 136 ++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 src/Application/Digital/Commands/RestoreBinEntry.cs diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index b12b9a93..24e7ebc3 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -17,6 +17,28 @@ public BinController(ICurrentUserService currentUserService) _currentUserService = currentUserService; } + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPut("entries/{entryId:guid}/restore")] + public async Task>> RestoreBinEntry( + [FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + + var command = new RestoreBinEntry.Command() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// /// diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index a74f0d5c..a99cbdf5 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -110,12 +110,12 @@ private static async void HandleInvalidOperationException(HttpContext context, E context.Response.StatusCode = StatusCodes.Status409Conflict; await WriteExceptionMessageAsync(context, ex); } - + private static async void HandleNotChangedException(HttpContext context, Exception ex) { context.Response.StatusCode = StatusCodes.Status204NoContent; } - + private static async Task WriteExceptionMessageAsync(HttpContext context, Exception ex) { await context.Response.Body.WriteAsync(SerializeToUtf8BytesWeb(Result.Fail(ex))); diff --git a/src/Application/Digital/Commands/RestoreBinEntry.cs b/src/Application/Digital/Commands/RestoreBinEntry.cs new file mode 100644 index 00000000..1e5c58ba --- /dev/null +++ b/src/Application/Digital/Commands/RestoreBinEntry.cs @@ -0,0 +1,136 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class RestoreBinEntry +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + private const string BinString = "_bin"; + + public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.Owner) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var entryPath = entry.Path; + var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); + var binCheck = entryPath.Substring(0, firstSlashIndex); + + if (!binCheck.Contains(BinString)) + { + throw new NotChangedException("Entry is not in bin."); + } + + if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) + { + throw new NotAllowedException("You do not have the permission to restore this entry."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var entryCheckPath = entryPath.Replace(binCheck, ""); + + var entryCheck = await _context.Entries.FirstOrDefaultAsync(x => + (x.Path.Equals("/") ? x.Path + x.Name : x.Path + "/" + x.Name).Equals(entryCheckPath.Equals("/") ? + entryCheckPath + entry.Name : entryCheckPath + "/" + entry.Name), cancellationToken); + + if (entryCheck is not null) + { + entryCheck.LastModified = localDateTimeNow; + entryCheck.LastModifiedBy = request.CurrentUser.Id; + var dupeResult = _context.Entries.Update(entryCheck); + _context.Entries.Remove(entry); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(dupeResult.Entity); + } + + var splitPath = entry.Path.Split("/"); + var currentPath = "/"; + foreach (var node in splitPath) + { + if (Array.IndexOf(splitPath, node) == 0) + { + continue; + } + + var entrySearch = await _context.Entries.FirstOrDefaultAsync(x => x.Path.Equals(currentPath) + && x.Name.Equals(node), cancellationToken); + + if (entrySearch is not null) + { + continue; + } + + var parentEntry = new Entry() + { + Name = node, + Path = currentPath, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, + OwnerId = request.CurrentUser.Id, + Owner = request.CurrentUser, + Uploader = request.CurrentUser, + }; + await _context.Entries.AddAsync(parentEntry, cancellationToken); + currentPath = currentPath.Equals("/") ? currentPath + node : currentPath + "/" + node; + } + + if (entry.IsDirectory) + { + var path = entry.Path[^1].Equals('/') ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var pattern = $"{path}/%"; + var childEntries = _context.Entries.Where(x => + x.Path.Trim().Equals(path) + || EF.Functions.Like(x.Path, pattern)); + + foreach (var childEntry in childEntries) + { + childEntry.Path = childEntry.Path.Replace(binCheck, ""); + childEntry.LastModified = localDateTimeNow; + childEntry.LastModifiedBy = request.CurrentUser.Id; + _context.Entries.Update(childEntry); + } + } + + entry.Path = entryPath.Replace(binCheck, ""); + entry.LastModified = localDateTimeNow; + entry.LastModifiedBy = request.CurrentUser.Id; + + var result = _context.Entries.Update(entry); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} From 9753c97b9207ac3a2a2ce258ee1b75eaaea19a72 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Mon, 10 Jul 2023 21:29:25 +0700 Subject: [PATCH 109/162] hotfix: bug in role auth (#354) --- src/Api/Controllers/DocumentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 74fdeb59..7cd13015 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -25,7 +25,7 @@ public DocumentsController(ICurrentUserService currentUserService) /// /// Id of the document to be retrieved /// A DocumentDto of the retrieved document - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] From 40686a48c6489010c9f1b122d11534dca19e8190 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:05:03 +0700 Subject: [PATCH 110/162] refactor: wrap get all departments with items (#355) * refactor: wrap get all departments with items * im stoopid --- src/Api/Controllers/DepartmentsController.cs | 8 ++++---- src/Application/Departments/Queries/GetAllDepartments.cs | 9 +++++---- .../Departments/Queries/GetAllDepartmentsTests.cs | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 37716a83..34ea8fd6 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -76,17 +76,17 @@ public async Task>>> GetRoomByDepartmen } /// - /// Get all documents + /// Get all departments /// - /// A list of DocumentDto + /// A list of DepartmentDto [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAll() + public async Task>>> GetAll() { var result = await Mediator.Send(new GetAllDepartments.Query()); - return Ok(Result>.Succeed(result)); + return Ok(Result>.Succeed(result)); } /// diff --git a/src/Application/Departments/Queries/GetAllDepartments.cs b/src/Application/Departments/Queries/GetAllDepartments.cs index 1a2f85c1..c4ca0e9c 100644 --- a/src/Application/Departments/Queries/GetAllDepartments.cs +++ b/src/Application/Departments/Queries/GetAllDepartments.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Application.Common.Interfaces; +using Application.Common.Models; using Application.Common.Models.Dtos; using AutoMapper; using MediatR; @@ -9,9 +10,9 @@ namespace Application.Departments.Queries; public class GetAllDepartments { - public record Query : IRequest>; + public record Query : IRequest>; - public class QueryHandler : IRequestHandler> + public class QueryHandler : IRequestHandler> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -22,11 +23,11 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Query request, CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { var departments = await _context.Departments .ToListAsync(cancellationToken); - var result = new ReadOnlyCollection(_mapper.Map>(departments)); + var result = new ItemsResult(_mapper.Map>(departments)); return result; } } diff --git a/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs b/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs index f126e367..8f5ba752 100644 --- a/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs +++ b/tests/Application.Tests.Integration/Departments/Queries/GetAllDepartmentsTests.cs @@ -35,7 +35,7 @@ public async Task ShouldReturnDepartments_WhenDepartmentsExist() var result = await SendAsync(query); // Assert - result.Should().ContainEquivalentOf(_mapper.Map(department)); + result.Items.Should().ContainEquivalentOf(_mapper.Map(department)); // Cleanup Remove(department); @@ -51,6 +51,6 @@ public async Task ShouldReturnEmptyList_WhenNoDepartmentsExist() var result = await SendAsync(query); // Assert - result.Count().Should().Be(0); + result.Items.Count().Should().Be(0); } } \ No newline at end of file From 469fb9548898775f37422f8d584d8edee88babf0 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Mon, 10 Jul 2023 23:01:53 +0700 Subject: [PATCH 111/162] fix: mismapping import request entity with dto (#352) --- src/Application/ImportRequests/Commands/AssignDocument.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index b01ff526..2dab47e0 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -103,7 +103,8 @@ public async Task Handle(Command request, CancellationToken ca UserId = request.CurrentUser.Id, Action = FolderLogMessage.AssignDocument, }; - var result = _context.Documents.Update(importRequest.Document); + _context.Documents.Update(importRequest.Document); + var result = _context.ImportRequests.Update(importRequest); _context.Folders.Update(folder); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.FolderLogs.AddAsync(folderLog, cancellationToken); From 99e4d84e1361cf319f1c46e26bd456dc06998b3f Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Tue, 11 Jul 2023 00:01:42 +0700 Subject: [PATCH 112/162] feat: implement moving entry to trash bin (#320) * feat: implement moving entry to trash bin * refactoring * fix: now will move child entries into bin * Update MoveEntryToBin.cs * add constraint * Update MoveEntryToBin.cs * Update MoveEntryToBin.cs --------- Co-authored-by: Nguyen Quang Chien --- src/Api/Controllers/BinController.cs | 28 +++++- .../Digital/Commands/MoveEntryToBin.cs | 91 +++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/Application/Digital/Commands/MoveEntryToBin.cs diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index 24e7ebc3..f6555727 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -16,11 +16,33 @@ public BinController(ICurrentUserService currentUserService) { _currentUserService = currentUserService; } - + + /// + /// Delete an entry + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("entries")] + public async Task>> MoveEntryToBin( + [FromQuery] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + + var command = new MoveEntryToBin.Command() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// /// - /// + /// /// [RequiresRole(IdentityData.Roles.Employee)] [HttpPut("entries/{entryId:guid}/restore")] @@ -56,7 +78,7 @@ public async Task>> DeleteBinEntry( CurrentUser = currentUser, EntryId = entryId, }; - + var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } diff --git a/src/Application/Digital/Commands/MoveEntryToBin.cs b/src/Application/Digital/Commands/MoveEntryToBin.cs new file mode 100644 index 00000000..b48ce0e3 --- /dev/null +++ b/src/Application/Digital/Commands/MoveEntryToBin.cs @@ -0,0 +1,91 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Digital.Commands; + +public class MoveEntryToBin +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + private const string BinString = "_bin"; + + public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider) + { + _mapper = mapper; + _context = context; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x=> x.Owner) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var entryPath = entry.Path; + var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); + var binCheck = entryPath.Substring(0, firstSlashIndex); + + if (binCheck.Contains(BinString)) + { + throw new ConflictException("Entry is already in bin."); + } + + if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) + { + throw new NotAllowedException("You do not have permission to move this entry into bin."); + } + + var ownerUsername = entry.Owner.Username; + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + if (entry.IsDirectory) + { + var path = entry.Path.Equals("/") ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var pattern = $"{path}/%"; + var childEntries = _context.Entries.Where(x => + x.Path.Trim().Equals(path) + || EF.Functions.Like(x.Path, pattern)); + + foreach (var childEntry in childEntries) + { + var childBinPath = ownerUsername + BinString + childEntry.Path; + childEntry.Path = childBinPath; + childEntry.LastModified = localDateTimeNow; + childEntry.LastModifiedBy = request.CurrentUser.Id; + _context.Entries.Update(childEntry); + } + } + + var binPath = ownerUsername + BinString + entryPath; + entry.Path = binPath; + entry.LastModified = localDateTimeNow; + entry.LastModifiedBy = request.CurrentUser.Id; + + var result = _context.Entries.Update(entry); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} From f7a23918fdea260dbfee6d319b41a23a056b621b Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 11 Jul 2023 00:09:15 +0700 Subject: [PATCH 113/162] fix: filter issued status for get all documents for employee and get document by id (#356) --- .../Queries/GetAllDocumentsForEmployeePaginated.cs | 3 ++- src/Application/Documents/Queries/GetDocumentById.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs index ef5a6991..d325c437 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs @@ -50,7 +50,8 @@ public async Task> Handle(Query request, .Include(x => x.Department) .Include(x => x.Folder) .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room); + .ThenInclude(z => z.Room) + .Where(x => x.Status != DocumentStatus.Issued); if (request.IsPrivate) { diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index e8d35247..e1566cd4 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -7,6 +7,7 @@ using AutoMapper; using Domain.Entities; using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -51,6 +52,11 @@ public async Task Handle(Query request, CancellationToken cancellat throw new KeyNotFoundException("Document does not exist."); } + if (request.CurrentUser.Role.IsEmployee() && document.Status == DocumentStatus.Issued) + { + throw new KeyNotFoundException("Document does not exist."); + } + if (ViolateConstraintForStaffAndEmployee(request.CurrentUser, document)) { throw new UnauthorizedAccessException("You don't have permission to view this document."); } From aea7adb1f2023d857f78935f7d2899f9bd46f9d5 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Tue, 11 Jul 2023 00:15:23 +0700 Subject: [PATCH 114/162] hotfix: improper import request status in checkin (#357) --- src/Application/ImportRequests/Commands/CheckinDocument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index 04c8b607..4d37f9ca 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -112,6 +112,6 @@ public async Task Handle(Command request, CancellationToken cancell } private static bool StatusesAreNotValid(DocumentStatus documentStatus, ImportRequestStatus importRequestStatus) - => documentStatus is not DocumentStatus.Issued || importRequestStatus is not ImportRequestStatus.Approved; + => documentStatus is not DocumentStatus.Issued || importRequestStatus is not ImportRequestStatus.Assigned; } } \ No newline at end of file From d3698112dc0d507a8a150e270133675056c5d8c7 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 11 Jul 2023 04:13:39 +0700 Subject: [PATCH 115/162] refactor: merge digital and entries packages to 1 packages named entries (#360) --- src/Api/Api.csproj | 4 ++++ src/Api/Controllers/BinController.cs | 2 +- src/Api/Controllers/EntriesController.cs | 7 +++---- .../GetAllEntriesPaginatedQueryParameters.cs | 9 --------- .../Entries/GetAllEntriesPaginatedQueryParameters.cs | 7 ++----- .../GetAllSharedEntriesPaginatedQueryParameters.cs | 12 ++++++++++++ .../ShareEntryPermissionRequest.cs | 2 +- .../UploadDigitalFileRequest.cs | 2 +- .../UploadSharedEntryRequest.cs | 2 +- src/Api/Controllers/SharedController.cs | 6 ++---- .../{Digital => Entries}/Commands/DeleteBinEntry.cs | 3 +-- .../Commands/DownloadDigitalFile.cs | 5 +---- .../{Digital => Entries}/Commands/MoveEntryToBin.cs | 2 +- .../{Digital => Entries}/Commands/RestoreBinEntry.cs | 3 +-- .../{Digital => Entries}/Commands/ShareEntry.cs | 2 +- .../{Digital => Entries}/Commands/UpdateEntry.cs | 3 +-- .../Commands/UploadDigitalFile.cs | 2 +- .../Commands/UploadSharedEntry.cs | 2 +- .../Queries/GetAllEntriesPaginated.cs | 2 +- .../{Digital => Entries}/Queries/GetEntryById.cs | 2 +- 20 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Entries/GetAllSharedEntriesPaginatedQueryParameters.cs rename src/Api/Controllers/Payload/Requests/{DigitalFile => Entries}/ShareEntryPermissionRequest.cs (84%) rename src/Api/Controllers/Payload/Requests/{DigitalFile => Entries}/UploadDigitalFileRequest.cs (81%) rename src/Api/Controllers/Payload/Requests/{DigitalFile => Entries}/UploadSharedEntryRequest.cs (77%) rename src/Application/{Digital => Entries}/Commands/DeleteBinEntry.cs (97%) rename src/Application/{Digital => Entries}/Commands/DownloadDigitalFile.cs (95%) rename src/Application/{Digital => Entries}/Commands/MoveEntryToBin.cs (98%) rename src/Application/{Digital => Entries}/Commands/RestoreBinEntry.cs (98%) rename src/Application/{Digital => Entries}/Commands/ShareEntry.cs (99%) rename src/Application/{Digital => Entries}/Commands/UpdateEntry.cs (97%) rename src/Application/{Digital => Entries}/Commands/UploadDigitalFile.cs (99%) rename src/Application/{Digital => Entries}/Commands/UploadSharedEntry.cs (99%) rename src/Application/{Digital => Entries}/Queries/GetAllEntriesPaginated.cs (98%) rename src/Application/{Digital => Entries}/Queries/GetEntryById.cs (96%) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index d45ec98c..4505ce9f 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index f6555727..81d37689 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -1,7 +1,7 @@ using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; -using Application.Digital.Commands; +using Application.Entries.Commands; using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 1dcc63f4..12ea545d 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -1,16 +1,15 @@ -using Api.Controllers.Payload.Requests.DigitalFile; using Api.Controllers.Payload.Requests.Entries; using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; -using Application.Digital.Commands; -using Application.Digital.Queries; +using Application.Entries.Commands; +using Application.Entries.Queries; using Application.Identity; using FluentValidation.Results; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; -using GetAllEntriesPaginatedQueryParameters = Api.Controllers.Payload.Requests.DigitalFile.GetAllEntriesPaginatedQueryParameters; +using GetAllEntriesPaginatedQueryParameters = Api.Controllers.Payload.Requests.Entries.GetAllEntriesPaginatedQueryParameters; namespace Api.Controllers; diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs deleted file mode 100644 index 873bd222..00000000 --- a/src/Api/Controllers/Payload/Requests/DigitalFile/GetAllEntriesPaginatedQueryParameters.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Api.Controllers.Payload.Requests.DigitalFile; - -/// -/// Get All Entries Paginated Query Parameters -/// -public class GetAllEntriesPaginatedQueryParameters : PaginatedQueryParameters -{ - public string EntryPath { get; set; } -} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs index 7fb1d6bc..462cbbc0 100644 --- a/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs @@ -1,12 +1,9 @@ -namespace Api.Controllers.Payload.Requests.Entries; +namespace Api.Controllers.Payload.Requests.Entries; /// /// Get All Entries Paginated Query Parameters /// public class GetAllEntriesPaginatedQueryParameters : PaginatedQueryParameters { - /// - /// Entry id - /// - public Guid? EntryId { get; set; } + public string EntryPath { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Entries/GetAllSharedEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Entries/GetAllSharedEntriesPaginatedQueryParameters.cs new file mode 100644 index 00000000..309f67c6 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Entries/GetAllSharedEntriesPaginatedQueryParameters.cs @@ -0,0 +1,12 @@ +namespace Api.Controllers.Payload.Requests.Entries; + +/// +/// Get All Shared Entries Paginated Query Parameters +/// +public class GetAllSharedEntriesPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Entry id + /// + public Guid? EntryId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs similarity index 84% rename from src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs rename to src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs index 5a01d6c4..ac50c522 100644 --- a/src/Api/Controllers/Payload/Requests/DigitalFile/ShareEntryPermissionRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs @@ -1,4 +1,4 @@ -namespace Api.Controllers.Payload.Requests.DigitalFile; +namespace Api.Controllers.Payload.Requests.Entries; public class ShareEntryPermissionRequest { diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs similarity index 81% rename from src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs rename to src/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs index 08f04215..ff4ddbf9 100644 --- a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadDigitalFileRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs @@ -1,4 +1,4 @@ -namespace Api.Controllers.Payload.Requests.DigitalFile; +namespace Api.Controllers.Payload.Requests.Entries; /// /// diff --git a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs similarity index 77% rename from src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs rename to src/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs index ab02aeed..21363822 100644 --- a/src/Api/Controllers/Payload/Requests/DigitalFile/UploadSharedEntryRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs @@ -1,4 +1,4 @@ -namespace Api.Controllers.Payload.Requests.DigitalFile; +namespace Api.Controllers.Payload.Requests.Entries; /// /// diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 9290917d..2c10fb15 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -1,18 +1,16 @@ -using Api.Controllers.Payload.Requests.DigitalFile; using Api.Controllers.Payload.Requests.Entries; using Application.Common.Exceptions; using Api.Controllers.Payload.Requests.Users; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; -using Application.Digital.Commands; +using Application.Entries.Commands; using Application.Entries.Queries; using Application.Identity; using FluentValidation.Results; using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; -using GetAllEntriesPaginatedQueryParameters = Api.Controllers.Payload.Requests.Entries.GetAllEntriesPaginatedQueryParameters; namespace Api.Controllers; @@ -53,7 +51,7 @@ public async Task DownloadSharedFile([FromRoute] Guid entryId) [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries")] public async Task>> GetAll( - [FromQuery] GetAllEntriesPaginatedQueryParameters queryParameters) + [FromQuery] GetAllSharedEntriesPaginatedQueryParameters queryParameters) { var currentUser = _currentUserService.GetCurrentUser(); var query = new GetAllSharedEntriesPaginated.Query() diff --git a/src/Application/Digital/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs similarity index 97% rename from src/Application/Digital/Commands/DeleteBinEntry.cs rename to src/Application/Entries/Commands/DeleteBinEntry.cs index 3012351e..296dffde 100644 --- a/src/Application/Digital/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -5,9 +5,8 @@ using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; -using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class DeleteBinEntry { diff --git a/src/Application/Digital/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs similarity index 95% rename from src/Application/Digital/Commands/DownloadDigitalFile.cs rename to src/Application/Entries/Commands/DownloadDigitalFile.cs index 1525af0e..24488ed6 100644 --- a/src/Application/Digital/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -1,13 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; -using Application.Common.Models.Dtos; using Application.Common.Models.Operations; -using AutoMapper; -using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class DownloadDigitalFile { diff --git a/src/Application/Digital/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs similarity index 98% rename from src/Application/Digital/Commands/MoveEntryToBin.cs rename to src/Application/Entries/Commands/MoveEntryToBin.cs index b48ce0e3..5b4d3b4f 100644 --- a/src/Application/Digital/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class MoveEntryToBin { diff --git a/src/Application/Digital/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs similarity index 98% rename from src/Application/Digital/Commands/RestoreBinEntry.cs rename to src/Application/Entries/Commands/RestoreBinEntry.cs index 1e5c58ba..113d3d29 100644 --- a/src/Application/Digital/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -1,6 +1,5 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; -using Application.Common.Models; using Application.Common.Models.Dtos.Digital; using AutoMapper; using Domain.Entities; @@ -9,7 +8,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class RestoreBinEntry { diff --git a/src/Application/Digital/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs similarity index 99% rename from src/Application/Digital/Commands/ShareEntry.cs rename to src/Application/Entries/Commands/ShareEntry.cs index 9e2715a7..15bb49cc 100644 --- a/src/Application/Digital/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class ShareEntry { diff --git a/src/Application/Digital/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs similarity index 97% rename from src/Application/Digital/Commands/UpdateEntry.cs rename to src/Application/Entries/Commands/UpdateEntry.cs index b456342f..4bef669d 100644 --- a/src/Application/Digital/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -3,12 +3,11 @@ using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; -using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class UpdateEntry { diff --git a/src/Application/Digital/Commands/UploadDigitalFile.cs b/src/Application/Entries/Commands/UploadDigitalFile.cs similarity index 99% rename from src/Application/Digital/Commands/UploadDigitalFile.cs rename to src/Application/Entries/Commands/UploadDigitalFile.cs index 43a074d0..5d3a31a2 100644 --- a/src/Application/Digital/Commands/UploadDigitalFile.cs +++ b/src/Application/Entries/Commands/UploadDigitalFile.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class UploadDigitalFile { public class Validator : AbstractValidator diff --git a/src/Application/Digital/Commands/UploadSharedEntry.cs b/src/Application/Entries/Commands/UploadSharedEntry.cs similarity index 99% rename from src/Application/Digital/Commands/UploadSharedEntry.cs rename to src/Application/Entries/Commands/UploadSharedEntry.cs index e3aab599..e75f162d 100644 --- a/src/Application/Digital/Commands/UploadSharedEntry.cs +++ b/src/Application/Entries/Commands/UploadSharedEntry.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Application.Digital.Commands; +namespace Application.Entries.Commands; public class UploadSharedEntry { diff --git a/src/Application/Digital/Queries/GetAllEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs similarity index 98% rename from src/Application/Digital/Queries/GetAllEntriesPaginated.cs rename to src/Application/Entries/Queries/GetAllEntriesPaginated.cs index 39bf17a0..1efa66fc 100644 --- a/src/Application/Digital/Queries/GetAllEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs @@ -7,7 +7,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace Application.Digital.Queries; +namespace Application.Entries.Queries; public class GetAllEntriesPaginated { diff --git a/src/Application/Digital/Queries/GetEntryById.cs b/src/Application/Entries/Queries/GetEntryById.cs similarity index 96% rename from src/Application/Digital/Queries/GetEntryById.cs rename to src/Application/Entries/Queries/GetEntryById.cs index 8e0e1615..e46b9b97 100644 --- a/src/Application/Digital/Queries/GetEntryById.cs +++ b/src/Application/Entries/Queries/GetEntryById.cs @@ -4,7 +4,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace Application.Digital.Queries; +namespace Application.Entries.Queries; public class GetEntryById { From 0a98e79e36772fdb0557479550547ae7352a3af9 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:56:50 +0700 Subject: [PATCH 116/162] update: users first times login with default password now are returned a token to reset password (#361) --- src/Api/Controllers/AuthController.cs | 50 ++++++++++++------- .../Responses/NotActivatedLoginResut.cs | 6 +++ src/Application/Application.csproj | 1 + .../Common/Interfaces/IIdentityService.cs | 4 +- .../Identity/IdentityService.cs | 27 +++++++++- src/Infrastructure/Infrastructure.csproj | 1 + 6 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 src/Api/Controllers/Payload/Responses/NotActivatedLoginResut.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 7d11f4f2..51c3d93c 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -5,6 +5,7 @@ using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; +using Application.Users.Queries; using Domain.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -32,26 +33,41 @@ public AuthController(IIdentityService identityService) [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task>> Login([FromBody] LoginModel loginModel) + public async Task Login([FromBody] LoginModel loginModel) { var result = await _identityService.LoginAsync(loginModel.Email, loginModel.Password); - - SetRefreshToken(result.AuthResult.RefreshToken); - SetJweToken(result.AuthResult.Token, result.AuthResult.RefreshToken); - var loginResult = new LoginResult() - { - Id = result.UserCredentials.Id, - Username = result.UserCredentials.Username, - Email = result.UserCredentials.Email, - Department = result.UserCredentials.Department, - Position = result.UserCredentials.Position, - Role = result.UserCredentials.Role, - FirstName = result.UserCredentials.FirstName, - LastName = result.UserCredentials.LastName, - }; - - return Ok(Result.Succeed(loginResult)); + return result.Match(loginSuccess => + { + SetRefreshToken(loginSuccess.AuthResult.RefreshToken); + SetJweToken(loginSuccess.AuthResult.Token, loginSuccess.AuthResult.RefreshToken); + + var loginResult = new LoginResult() + { + Id = loginSuccess.UserCredentials.Id, + Username = loginSuccess.UserCredentials.Username, + Email = loginSuccess.UserCredentials.Email, + Department = loginSuccess.UserCredentials.Department, + Position = loginSuccess.UserCredentials.Position, + Role = loginSuccess.UserCredentials.Role, + FirstName = loginSuccess.UserCredentials.FirstName, + LastName = loginSuccess.UserCredentials.LastName, + }; + + return Ok(Result.Succeed(loginResult)); + }, + token => + { + var r = new Result + { + Data = new NotActivatedLoginResult() + { + Token = token, + } + }; + return Unauthorized(r); + }); + } /// diff --git a/src/Api/Controllers/Payload/Responses/NotActivatedLoginResut.cs b/src/Api/Controllers/Payload/Responses/NotActivatedLoginResut.cs new file mode 100644 index 00000000..d57edbfe --- /dev/null +++ b/src/Api/Controllers/Payload/Responses/NotActivatedLoginResut.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Responses; + +public class NotActivatedLoginResult +{ + public string Token { get; set; } +} \ No newline at end of file diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 342368c9..8853c8ca 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index c4d91102..03fc7f21 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -1,5 +1,7 @@ using Application.Common.Models; using Application.Users.Queries; +using Domain.Entities; +using OneOf; namespace Application.Common.Interfaces; @@ -7,7 +9,7 @@ public interface IIdentityService { Task Validate(string token, string refreshToken); Task RefreshTokenAsync(string token, string refreshToken); - Task<(AuthenticationResult AuthResult, UserDto UserCredentials)> LoginAsync(string email, string password); + Task> LoginAsync(string email, string password); Task LogoutAsync(string token, string refreshToken); Task ResetPassword(string token, string newPassword); } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index ddf84780..b7b91c23 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using NodaTime; +using OneOf; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; namespace Infrastructure.Identity; @@ -191,7 +192,7 @@ public async Task RefreshTokenAsync(string token, string r } } - public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) + public async Task> LoginAsync(string email, string password) { var user = _applicationDbContext.Users .Include(x => x.Department) @@ -201,6 +202,30 @@ public async Task RefreshTokenAsync(string token, string r { throw new AuthenticationException("Username or password is invalid."); } + + if (!user.IsActivated) + { + var token = Guid.NewGuid().ToString(); + var expirationDate = LocalDateTime.FromDateTime(DateTime.Now.AddDays(1)); + var resetPasswordToken = new ResetPasswordToken() + { + User = user, + TokenHash = SecurityUtil.Hash(token), + ExpirationDate = expirationDate, + IsInvalidated = false, + }; + + var tokens = _authDbContext.ResetPasswordTokens.Where(x => x.User.Id == user.Id && !x.IsInvalidated); + + foreach (var t in tokens) + { + t.IsInvalidated = true; + } + await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken); + await _authDbContext.SaveChangesAsync(CancellationToken.None); + + return token; + } var existedRefreshTokens = _authDbContext.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 82d1f679..e5c36a93 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -13,6 +13,7 @@ + From f096f3746dfcbe46027d9f9c210ce2e4406876ed Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:47:23 +0700 Subject: [PATCH 117/162] update: merge background services into one class (#311) Because when registering more than 2 services, some anomalies happen with the stopping token, causing them to crash the app, combining all workers into 1 method solves the problem --- src/Api/ConfigureServices.cs | 2 +- src/Api/Services/BackgroundWorkers.cs | 54 +++++++++++++++++++++ src/Api/Services/ExpiryPermissionService.cs | 30 ------------ 3 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 src/Api/Services/BackgroundWorkers.cs delete mode 100644 src/Api/Services/ExpiryPermissionService.cs diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index c0029e4d..390e032b 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -18,7 +18,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services // Register services services.AddServices(); - services.AddHostedService(); + services.AddHostedService(); services.AddControllers(opt => opt.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()))); diff --git a/src/Api/Services/BackgroundWorkers.cs b/src/Api/Services/BackgroundWorkers.cs new file mode 100644 index 00000000..9ae66e23 --- /dev/null +++ b/src/Api/Services/BackgroundWorkers.cs @@ -0,0 +1,54 @@ +using Application.Common.Interfaces; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Api.Services; + +public class BackgroundWorkers : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public BackgroundWorkers(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var workers = new List + { + DisposeExpiredEntries(TimeSpan.FromSeconds(10), stoppingToken), + DisposeExpiredPermissions(TimeSpan.FromSeconds(10), stoppingToken) + }; + + await Task.WhenAll(workers.ToArray()); + } + } + + private async Task DisposeExpiredEntries(TimeSpan delay, CancellationToken stoppingToken) + { + var expiryLocalDateTime = LocalDateTime.FromDateTime(DateTime.Now).PlusDays(-30); + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var entries = context.Entries.Where(x => + !EF.Functions.ILike(x.Path, "/%") && x.LastModified!.Value < expiryLocalDateTime); + context.Entries.RemoveRange(entries); + await context.SaveChangesAsync(stoppingToken); + await Task.Delay(delay, stoppingToken); + } + + private async Task DisposeExpiredPermissions(TimeSpan delay, CancellationToken stoppingToken) + { + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var expiredPermissions = context.Permissions.Where(x => x.ExpiryDateTime < localDateTimeNow); + context.Permissions.RemoveRange(expiredPermissions); + await context.SaveChangesAsync(stoppingToken); + await Task.Delay(delay, stoppingToken); + } +} \ No newline at end of file diff --git a/src/Api/Services/ExpiryPermissionService.cs b/src/Api/Services/ExpiryPermissionService.cs deleted file mode 100644 index 360c108e..00000000 --- a/src/Api/Services/ExpiryPermissionService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Application.Common.Interfaces; -using Domain.Entities.Physical; -using NodaTime; - -namespace Api.Services; - -public class ExpiryPermissionService : BackgroundService -{ - private readonly IServiceProvider _serviceProvider; - - public ExpiryPermissionService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - using var scope = _serviceProvider.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - var expiredPermissions = context.Permissions.Where(x => x.ExpiryDateTime < localDateTimeNow); - context.Permissions.RemoveRange(expiredPermissions); - await context.SaveChangesAsync(stoppingToken); - await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); - } - } -} \ No newline at end of file From f2796f1d234c04081dcad2e58907eef6a93f58d4 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:48:43 +0700 Subject: [PATCH 118/162] feat: add logs to entries (#362) --- .../Common/Messages/EntryLogMessages.cs | 13 ++++++++ .../Entries/Commands/DeleteBinEntry.cs | 11 ++++++- .../Entries/Commands/DownloadDigitalFile.cs | 12 ++++++- .../Entries/Commands/MoveEntryToBin.cs | 10 +++++- .../Entries/Commands/RestoreBinEntry.cs | 11 +++++-- .../Entries/Commands/ShareEntry.cs | 11 +++++-- .../Entries/Commands/UpdateEntry.cs | 12 +++++-- .../Entries/Commands/UploadDigitalFile.cs | 11 +++++-- .../Entries/Commands/UploadSharedEntry.cs | 11 +++++-- src/Application/Entries/EntryLogExtension.cs | 33 +++++++++++++++++++ 10 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 src/Application/Common/Messages/EntryLogMessages.cs create mode 100644 src/Application/Entries/EntryLogExtension.cs diff --git a/src/Application/Common/Messages/EntryLogMessages.cs b/src/Application/Common/Messages/EntryLogMessages.cs new file mode 100644 index 00000000..b03c8cda --- /dev/null +++ b/src/Application/Common/Messages/EntryLogMessages.cs @@ -0,0 +1,13 @@ +namespace Application.Common.Messages; + +public class EntryLogMessages +{ + public const string DeleteBinEntry = "User with Id {UserId} delete entry with Id {EntryId}"; + public const string DownloadDigitalFile = "User with Id {UserId} download file with Id {FileId}"; + public const string MoveEntryToBin = "User with Id {UserId} move entry with Id {EntryId} to bin"; + public const string RestoreBinEntry = "User with Id {UserId} restore entry with Id {EntryId} from bin"; + public const string ShareEntry = "User with Id {UserId} share entry with Id {EntryId} to User with id {SharedUserId}"; + public const string UpdateEntry = "User with Id {UserId} update entry with Id {EntryId}"; + public const string UploadDigitalFile = "User with Id {UserId} upload an entry with id {EntryId}"; + public const string UploadSharedEntry = "User with Id {UserId} upload an emtry with id {EntryId}"; +} \ No newline at end of file diff --git a/src/Application/Entries/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs index 296dffde..071f35c7 100644 --- a/src/Application/Entries/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Application.Entries.Commands; @@ -22,12 +25,14 @@ public class Handler : IRequestHandler private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; private const string BinString = "_bin"; + private readonly ILogger _logger; - public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider) + public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider, ILogger logger) { _mapper = mapper; _context = context; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -71,6 +76,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = _context.Entries.Remove(entry); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) + { + _logger.LogDeleteBinEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 24488ed6..11715b84 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -1,8 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Operations; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Application.Entries.Commands; @@ -25,10 +27,12 @@ public class Result public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context) + public CommandHandler(IApplicationDbContext context, ILogger logger) { _context = context; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -66,6 +70,12 @@ public async Task Handle(Command request, CancellationToken cancellation var content = new MemoryStream(entry.File!.FileData); var fileType = entry.File!.FileType; var fileExtension = entry.File!.FileExtension; + var fileId = entry.File!.Id; + + using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUserId)) + { + _logger.LogDownLoadFile(request.CurrentUserId.ToString(), fileId.ToString()); + } return new Result() { diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index 5b4d3b4f..287063df 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -1,10 +1,12 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using AutoMapper; using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -23,12 +25,14 @@ public class Handler : IRequestHandler private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; private const string BinString = "_bin"; + private readonly ILogger _logger; - public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider) + public Handler(IMapper mapper, IApplicationDbContext context, IDateTimeProvider dateTimeProvider, ILogger logger) { _mapper = mapper; _context = context; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -85,6 +89,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = _context.Entries.Update(entry); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUser.Id)) + { + _logger.LogMoveEntryToBin(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index 113d3d29..14b7c16d 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -1,11 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using AutoMapper; using Domain.Entities; using Domain.Entities.Digital; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -24,12 +26,13 @@ public class Handler : IRequestHandler private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; private const string BinString = "_bin"; - - public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + private readonly ILogger _logger; + public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -129,6 +132,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = _context.Entries.Update(entry); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) + { + _logger.LogRestoreBinEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs index 15bb49cc..099100d4 100644 --- a/src/Application/Entries/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -1,6 +1,7 @@ using System.Configuration; using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; @@ -8,6 +9,7 @@ using Domain.Entities.Digital; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -31,12 +33,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = applicationDbContext; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -96,7 +100,10 @@ public async Task Handle(Command request, CancellationToken } } await _context.SaveChangesAsync(cancellationToken); - + using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) + { + _logger.LogShareEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString(), request.UserId.ToString()); + } var result = await _context.EntryPermissions.FirstOrDefaultAsync(x => x.EntryId == request.EntryId && x.EmployeeId == request.UserId, cancellationToken); diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index 4bef669d..9341746f 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; +using Domain.Entities.Digital; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -23,12 +26,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -75,7 +80,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = _context.Entries.Update(entry); await _context.SaveChangesAsync(cancellationToken); - + using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUserId)) + { + _logger.LogUpdateEntry(request.CurrentUserId.ToString(), entry.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/Commands/UploadDigitalFile.cs b/src/Application/Entries/Commands/UploadDigitalFile.cs index 5d3a31a2..eb6107c7 100644 --- a/src/Application/Entries/Commands/UploadDigitalFile.cs +++ b/src/Application/Entries/Commands/UploadDigitalFile.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using Application.Helpers; using AutoMapper; @@ -8,6 +9,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -44,12 +46,14 @@ public class Handler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; - public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -116,7 +120,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = await _context.Entries.AddAsync(entryEntity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - + using (Logging.PushProperties(nameof(Entry), entryEntity.Id, request.CurrentUser.Id)) + { + _logger.LogUploadDigitalFile(request.CurrentUser.Id.ToString(), entryEntity.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/Commands/UploadSharedEntry.cs b/src/Application/Entries/Commands/UploadSharedEntry.cs index e75f162d..9e06a5f0 100644 --- a/src/Application/Entries/Commands/UploadSharedEntry.cs +++ b/src/Application/Entries/Commands/UploadSharedEntry.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; @@ -8,6 +9,7 @@ using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Entries.Commands; @@ -40,12 +42,13 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + private readonly ILogger _logger; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; _dateTimeProvider = dateTimeProvider; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -124,6 +127,10 @@ public async Task Handle(Command request, CancellationToken cancellati var result = await _context.Entries.AddAsync(entity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); + using (Logging.PushProperties(nameof(Entry), entity.Id, request.CurrentUser.Id)) + { + _logger.LogUploadSharedEntry(request.CurrentUser.Id.ToString(), entity.Id.ToString()); + } return _mapper.Map(result.Entity); } } diff --git a/src/Application/Entries/EntryLogExtension.cs b/src/Application/Entries/EntryLogExtension.cs new file mode 100644 index 00000000..28e4d2c1 --- /dev/null +++ b/src/Application/Entries/EntryLogExtension.cs @@ -0,0 +1,33 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Entries; + +public static partial class EntryLogExtension +{ + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.DownloadDigitalFile, EventId = Common.Logging.EventId.Other)] + public static partial void LogDownLoadFile(this ILogger logger, string userId, string fileId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.MoveEntryToBin, EventId = Common.Logging.EventId.Update)] + public static partial void LogMoveEntryToBin(this ILogger logger, string userId, string entryId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.ShareEntry, EventId = EventId.OtherRequestRelated)] + public static partial void LogShareEntry(this ILogger logger, string userId, string entryId, string sharedUserId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UpdateEntry, EventId = EventId.Update)] + public static partial void LogUpdateEntry(this ILogger logger, string userId, string entryId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadDigitalFile, EventId = EventId.Add)] + public static partial void LogUploadDigitalFile(this ILogger logger, string userId, string entryId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadSharedEntry, EventId = EventId.Add)] + public static partial void LogUploadSharedEntry(this ILogger logger, string userId, string entryId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.DeleteBinEntry, EventId = EventId.Remove)] + public static partial void LogDeleteBinEntry(this ILogger logger, string userId, string entryId); + + [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.RestoreBinEntry, EventId = EventId.Update)] + public static partial void LogRestoreBinEntry(this ILogger logger, string userId, string entryId); +} + From 8b02ec13981bd6f3b5cc6d3169803e28a86df4de Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:23:27 +0700 Subject: [PATCH 119/162] Delete/log entities (#364) * delete: all logging activities * delete: all logs entities * add: delete log entities migration --- src/Api/Controllers/LockersController.cs | 5 +- .../Commands/ApproveOrRejectBorrowRequest.cs | 26 +- .../Borrows/Commands/BorrowDocument.cs | 12 - .../Borrows/Commands/CancelBorrowRequest.cs | 11 - .../Borrows/Commands/CheckoutDocument.cs | 10 - .../Borrows/Commands/ReturnDocument.cs | 10 - .../Borrows/Commands/UpdateBorrow.cs | 12 - .../Queries/GetAllRequestLogsPaginated.cs | 2 - .../Interfaces/IApplicationDbContext.cs | 8 - .../Models/Dtos/Logging/DocumentLogDto.cs | 25 - .../Models/Dtos/Logging/FolderLogDto.cs | 22 - .../Models/Dtos/Logging/LockerLogDto.cs | 22 - .../Models/Dtos/Logging/RequestLogDto.cs | 25 - .../Common/Models/Dtos/Logging/RoomLogDto.cs | 25 - .../Common/Models/Dtos/Logging/UserLogDto.cs | 22 - .../Documents/Commands/DeleteDocument.cs | 11 - .../Documents/Commands/ImportDocument.cs | 13 - .../Documents/Commands/ShareDocument.cs | 19 - .../Documents/Commands/UpdateDocument.cs | 11 - src/Application/Folders/Commands/AddFolder.cs | 11 - .../Folders/Commands/RemoveFolder.cs | 11 - .../Folders/Commands/UpdateFolder.cs | 11 - .../Commands/ApproveOrRejectDocument.cs | 26 - .../ImportRequests/Commands/AssignDocument.cs | 20 - .../Commands/CheckinDocument.cs | 20 - .../Commands/RequestImportDocument.cs | 15 +- src/Application/Lockers/Commands/AddLocker.cs | 11 - .../Lockers/Commands/RemoveLocker.cs | 11 - .../Lockers/Commands/UpdateLocker.cs | 13 - src/Application/Rooms/Commands/AddRoom.cs | 12 - src/Application/Rooms/Commands/RemoveRoom.cs | 12 - src/Application/Rooms/Commands/UpdateRoom.cs | 12 - .../Staffs/Commands/AssignStaff.cs | 11 - .../Staffs/Commands/RemoveStaffFromRoom.cs | 11 - src/Application/Users/Commands/AddUser.cs | 13 - src/Application/Users/Commands/UpdateUser.cs | 11 - src/Domain/Entities/Logging/DocumentLog.cs | 9 - src/Domain/Entities/Logging/FolderLog.cs | 9 - src/Domain/Entities/Logging/LockerLog.cs | 9 - src/Domain/Entities/Logging/RequestLog.cs | 10 - src/Domain/Entities/Logging/RoomLog.cs | 8 - src/Domain/Entities/Logging/UserLog.cs | 7 - .../Persistence/ApplicationDbContext.cs | 9 - .../Configurations/UserLogConfiguration.cs | 20 - ...230711074257_DeleteLogEntities.Designer.cs | 886 ++++++++++++++++++ .../20230711074257_DeleteLogEntities.cs | 228 +++++ .../ApplicationDbContextModelSnapshot.cs | 258 ----- 47 files changed, 1118 insertions(+), 857 deletions(-) delete mode 100644 src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs delete mode 100644 src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs delete mode 100644 src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs delete mode 100644 src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs delete mode 100644 src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs delete mode 100644 src/Application/Common/Models/Dtos/Logging/UserLogDto.cs delete mode 100644 src/Domain/Entities/Logging/DocumentLog.cs delete mode 100644 src/Domain/Entities/Logging/FolderLog.cs delete mode 100644 src/Domain/Entities/Logging/LockerLog.cs delete mode 100644 src/Domain/Entities/Logging/RequestLog.cs delete mode 100644 src/Domain/Entities/Logging/RoomLog.cs delete mode 100644 src/Domain/Entities/Logging/UserLog.cs delete mode 100644 src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.cs diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index ea9f9ef1..58d1c502 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,9 +1,6 @@ -using Api.Controllers.Payload.Requests; -using Api.Controllers.Payload.Requests.Lockers; +using Api.Controllers.Payload.Requests.Lockers; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos; -using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Lockers.Commands; diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index a93e4e8d..ec9397ac 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -2,10 +2,8 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; -using Domain.Entities.Logging; using Domain.Enums; using Domain.Statuses; using MediatR; @@ -105,24 +103,6 @@ public async Task Handle(Command request, CancellationToken cancellat && (x.DueTime > localDateTimeNow || x.Status == BorrowRequestStatus.Overdue)); - var log = new DocumentLog() - { - ObjectId = borrowRequest.Document.Id, - UserId = currentUser!.Id, - User = currentUser, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.Approve, - }; - var requestLog = new RequestLog() - { - ObjectId = borrowRequest.Document.Id, - Type = RequestType.Borrow, - UserId = currentUser.Id, - User = currentUser, - Time = localDateTimeNow, - Action = RequestLogMessages.ApproveBorrow, - }; - if (request.Decision.IsApproval()) { foreach (var existedBorrow in existedBorrows) @@ -147,8 +127,6 @@ is BorrowRequestStatus.Approved if (request.Decision.IsRejection()) { borrowRequest.Status = BorrowRequestStatus.Rejected; - log.Action = DocumentLogMessages.Borrow.Reject; - requestLog.Action = RequestLogMessages.RejectBorrow; using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) { @@ -158,11 +136,9 @@ is BorrowRequestStatus.Approved borrowRequest.StaffReason = request.StaffReason; borrowRequest.LastModified = localDateTimeNow; - borrowRequest.LastModifiedBy = currentUser.Id; + borrowRequest.LastModifiedBy = request.CurrentUserId; var result = _context.Borrows.Update(borrowRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 873b5997..507f4420 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,12 +1,9 @@ -using System.Runtime.InteropServices; using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; using AutoMapper; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Events; using Domain.Statuses; @@ -160,16 +157,7 @@ is BorrowRequestStatus.Approved entity.Status = BorrowRequestStatus.Approved; } - var log = new DocumentLog() - { - UserId = user.Id, - User = user, - ObjectId = document.Id, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.NewBorrowRequest, - }; var result = await _context.Borrows.AddAsync(entity, cancellationToken); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("Request", document.Id, user.Id)) { diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index e20f15ba..6f9ffd79 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -1,10 +1,8 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; -using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -61,18 +59,9 @@ public async Task Handle(Command request, CancellationToken cancellat var currentUser = await _context.Users .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); - var log = new DocumentLog() - { - ObjectId = borrowRequest.Document.Id, - UserId = currentUser!.Id, - User = currentUser, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.Cancel, - }; borrowRequest.Status = BorrowRequestStatus.Cancelled; var result = _context.Borrows.Update(borrowRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) { diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index f7d3f2a9..275b344f 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -5,7 +5,6 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -86,17 +85,8 @@ public async Task Handle(Command request, CancellationToken cancellat borrowRequest.Document.Status = DocumentStatus.Borrowed; borrowRequest.Document.LastModified = localDateTimeNow; borrowRequest.Document.LastModifiedBy = request.CurrentStaff.Id; - var log = new DocumentLog() - { - ObjectId = borrowRequest.Document.Id, - UserId = request.CurrentStaff.Id, - User = request.CurrentStaff, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.Checkout, - }; var result = _context.Borrows.Update(borrowRequest); _context.Documents.Update(borrowRequest.Document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentStaff.Id)) { diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index 86332e27..d2e3cc37 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -5,7 +5,6 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -87,16 +86,7 @@ public async Task Handle(Command request, CancellationToken cancellat borrowRequest.Document.Status = DocumentStatus.Available; borrowRequest.ActualReturnTime = localDateTimeNow; - var log = new DocumentLog() - { - ObjectId = borrowRequest.Document.Id, - UserId = request.CurrentUser.Id, - User = request.CurrentUser, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.Checkout, - }; var result = _context.Borrows.Update(borrowRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); _context.Documents.Update(borrowRequest.Document); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index 7bbda719..0d1d9512 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -1,12 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; -using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; using MediatR; @@ -113,16 +110,7 @@ is BorrowRequestStatus.Approved borrowRequest.LastModified = localDateTimeNow; borrowRequest.LastModifiedBy = request.CurrentUser.Id; - var log = new DocumentLog() - { - UserId = request.CurrentUser.Id, - User = request.CurrentUser, - ObjectId = borrowRequest.Document.Id, - Time = localDateTimeNow, - Action = DocumentLogMessages.Borrow.UpdateBorrow, - }; var result = _context.Borrows.Update(borrowRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs index cb4b4b3b..f10ba63c 100644 --- a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -2,8 +2,6 @@ using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; -using Application.Common.Models.Dtos.Logging; -using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; using AutoMapper; using Domain.Enums; diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 37fa4728..014115df 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,7 +1,6 @@ using Application.Common.Models; using Domain.Entities; using Domain.Entities.Digital; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; @@ -27,12 +26,5 @@ public interface IApplicationDbContext public DbSet Logs { get; } - public DbSet RoomLogs { get; } - public DbSet LockerLogs { get; } - public DbSet FolderLogs { get; } - public DbSet DocumentLogs { get; } - public DbSet RequestLogs { get; } - public DbSet UserLogs { get; } - Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs deleted file mode 100644 index c7098277..00000000 --- a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class DocumentLogDto : BaseDto, IMapFrom -{ - public Guid UserId { get; set; } - public string Action { get; set; } - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } - - public void Mapping(Profile profile) - { - - profile.CreateMap() - .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); - - } -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs deleted file mode 100644 index f5287aea..00000000 --- a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class FolderLogDto : BaseDto, IMapFrom -{ - public string Action { get; set; } = null!; - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } = null!; - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); - } -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs deleted file mode 100644 index dc532aa7..00000000 --- a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class LockerLogDto : BaseDto, IMapFrom -{ - public string Action { get; set; } = null!; - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } = null!; - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember( dest => dest.Time, - opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); - } -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs deleted file mode 100644 index a874bd7a..00000000 --- a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class RequestLogDto : BaseDto, IMapFrom -{ - public string Action { get; set; } = null!; - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } = null!; - public string Type { get; set; } = null!; - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Type, - opt => opt.MapFrom(src => src.Type.ToString())); - } -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs deleted file mode 100644 index 8d05216a..00000000 --- a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Application.Common.Mappings; -using Application.Common.Models.Dtos.Physical; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class RoomLogDto : BaseDto, IMapFrom -{ - public Guid UserId { get; set; } - public string Action { get; set; } - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } - - public void Mapping(Profile profile) - { - - profile.CreateMap() - .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); - - } -} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs deleted file mode 100644 index 83a1d38a..00000000 --- a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Application.Common.Mappings; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; - -namespace Application.Common.Models.Dtos.Logging; - -public class UserLogDto : BaseDto, IMapFrom -{ - public string Action { get; set; } = null!; - public Guid? ObjectId { get; set; } - public DateTime Time { get; set; } - public UserDto User { get; set; } = null!; - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember( dest => dest.Time, - opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); - - } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index deeb88a9..0294711f 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,10 +1,8 @@ using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -55,16 +53,7 @@ public async Task Handle(Command request, CancellationToken cancell _context.Folders.Update(document.Folder); } - var log = new DocumentLog() - { - ObjectId = document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = DocumentLogMessages.Delete, - }; var result = _context.Documents.Remove(document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 4f2c19b9..b105ff93 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,13 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; -using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.ImportRequests; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; @@ -114,19 +111,9 @@ public async Task Handle(Command request, CancellationToken cancell Created = localDateTimeNow, CreatedBy = request.CurrentUser.Id, }; - var log = new DocumentLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = entity.Id, - Time = localDateTimeNow, - Action = DocumentLogMessages.Import.NewImport, - }; - var result = await _context.Documents.AddAsync(entity, cancellationToken); folder.NumberOfDocuments += 1; _context.Folders.Update(folder); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs index fbddc26b..c1359f80 100644 --- a/src/Application/Documents/Commands/ShareDocument.cs +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -1,12 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -74,22 +72,9 @@ public async Task Handle(Command request, CancellationToken cancell var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - var log = new DocumentLog() - { - ObjectId = document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = string.Empty, - }; - await HandlePermissionGrantOrRevoke(request.CanRead, document, DocumentOperation.Read, user, request.ExpiryDate.ToLocalTime(), request.CurrentUser.Id, cancellationToken); await HandlePermissionGrantOrRevoke(request.CanBorrow, document, DocumentOperation.Borrow, user, request.ExpiryDate.ToLocalTime(), request.CurrentUser.Id, cancellationToken); - if (!string.IsNullOrEmpty(log.Action)) - { - await _applicationDbContext.DocumentLogs.AddAsync(log, cancellationToken); - } await _applicationDbContext.SaveChangesAsync(cancellationToken); return _mapper.Map(document); } @@ -137,8 +122,6 @@ private async Task GrantPermission( case DocumentOperation.Borrow: _logger.LogGrantPermission(DocumentOperation.Borrow.ToString(), user.Username); break; - default: - break; } } } @@ -163,8 +146,6 @@ private async Task RevokePermission( case DocumentOperation.Borrow: _logger.LogRevokePermission(DocumentOperation.Borrow.ToString(), user.Username); break; - default: - break; } } } diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index fe52ba89..fc8e5aea 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -2,11 +2,9 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -103,16 +101,7 @@ public async Task Handle(Command request, CancellationToken cancell document.LastModified = localDateTimeNow; document.LastModifiedBy = request.CurrentUser.Id; - var log = new DocumentLog() - { - ObjectId = document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = DocumentLogMessages.Update, - }; var result = _context.Documents.Update(document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Document), result.Entity.Id, request.CurrentUser.Id)) diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 480a8a7c..2810e5f0 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -2,11 +2,9 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; @@ -109,18 +107,9 @@ public async Task Handle(Command request, CancellationToken cancellat CreatedBy = request.CurrentUser.Id, }; - var log = new FolderLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = entity.Id, - Time = localDateTimeNow, - Action = FolderLogMessage.Add, - }; var result = await _context.Folders.AddAsync(entity, cancellationToken); locker.NumberOfFolders += 1; _context.Lockers.Update(locker); - await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Folder), result.Entity.Id, request.CurrentUser.Id)) diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index a09af172..d9d24596 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -2,11 +2,9 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -69,17 +67,8 @@ public async Task Handle(Command request, CancellationToken cancellat var locker = folder.Locker; - var log = new FolderLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = folder.Id, - Time = localDateTimeNow, - Action = FolderLogMessage.Remove, - }; var result = _context.Folders.Remove(folder); locker.NumberOfFolders -= 1; - await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index b61efac9..ce5c058d 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -2,11 +2,9 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -101,16 +99,7 @@ public async Task Handle(Command request, CancellationToken cancellat folder.LastModifiedBy = request.CurrentUser.Id; folder.IsAvailable = request.IsAvailable; - var log = new FolderLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = folder.Id, - Time = localDateTimeNow, - Action = FolderLogMessage.Update, - }; var result = _context.Folders.Update(folder); - await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Folder), folder.Id, request.CurrentUser.Id)) diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index d6efabd9..9beb97d5 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -2,11 +2,9 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -96,29 +94,9 @@ public async Task Handle(Command request, CancellationToken ca var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - var log = new DocumentLog() - { - ObjectId = document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = DocumentLogMessages.Import.Approve, - }; - - var requestLog = new RequestLog() - { - ObjectId = document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = RequestLogMessages.ApproveImport, - }; - if (request.Decision.IsApproval()) { importRequest.Status = ImportRequestStatus.Approved; - log.Action = DocumentLogMessages.Import.Approve; - requestLog.Action = RequestLogMessages.ApproveImport; using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) { @@ -134,8 +112,6 @@ public async Task Handle(Command request, CancellationToken ca { importRequest.Status = ImportRequestStatus.Rejected; _context.Documents.Remove(importRequest.Document); - log.Action = DocumentLogMessages.Import.Reject; - requestLog.Action = RequestLogMessages.RejectImport; using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) { @@ -152,8 +128,6 @@ public async Task Handle(Command request, CancellationToken ca importRequest.LastModifiedBy = request.CurrentUser.Id; var result = _context.ImportRequests.Update(importRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index 2dab47e0..1397f4b5 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; @@ -87,27 +85,9 @@ public async Task Handle(Command request, CancellationToken ca folder.LastModified = localDateTimeNow; folder.LastModifiedBy = request.CurrentUser.Id; - var log = new DocumentLog() - { - ObjectId = importRequest.Document.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = DocumentLogMessages.Import.Assign, - }; - var folderLog = new FolderLog() - { - ObjectId = folder.Id, - Time = localDateTimeNow, - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - Action = FolderLogMessage.AssignDocument, - }; _context.Documents.Update(importRequest.Document); var result = _context.ImportRequests.Update(importRequest); _context.Folders.Update(folder); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.FolderLogs.AddAsync(folderLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Document), importRequest.Document.Id, request.CurrentUser.Id)) { diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index 4d37f9ca..39b78f6c 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; @@ -79,26 +77,8 @@ public async Task Handle(Command request, CancellationToken cancell importRequest.LastModified = localDateTimeNow; importRequest.LastModifiedBy = request.CurrentUser.Id; - var log = new DocumentLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = document.Id, - Time = localDateTimeNow, - Action = DocumentLogMessages.Import.Checkin, - }; - var importLog = new RequestLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = document.Id, - Time = localDateTimeNow, - Action = RequestLogMessages.CheckInImport, - }; var result = _context.Documents.Update(document); _context.ImportRequests.Update(importRequest); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.RequestLogs.AddAsync(importLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Document), document.Id, request.CurrentUser.Id)) { diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 5c1e7894..2e768164 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; @@ -94,20 +92,11 @@ public async Task Handle(Command request, CancellationToken ca StaffReason = string.Empty, }; - var log = new DocumentLog() - { - ObjectId = entity.Id, - Time = localDateTimeNow, - User = request.Issuer, - UserId = request.Issuer.Id, - Action = DocumentLogMessages.Import.NewImportRequest, - }; var result = await _context.ImportRequests.AddAsync(importRequest, cancellationToken); - var documentEntry = await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - using (Logging.PushProperties(nameof(Document), documentEntry.Entity.Id, request.Issuer.Id)) + using (Logging.PushProperties(nameof(Document), result.Entity.DocumentId, request.Issuer.Id)) { - _logger.LogAddDocument(documentEntry.Entity.Id.ToString()); + _logger.LogAddDocument(result.Entity.DocumentId.ToString()); } using (Logging.PushProperties(nameof(ImportRequest), importRequest.Id, request.Issuer.Id)) { diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 3bc3c3f3..41c558e6 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; @@ -100,17 +98,8 @@ public async Task Handle(Command request, CancellationToken cancellat var result = await _context.Lockers.AddAsync(entity, cancellationToken); - var log = new LockerLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = entity.Id, - Time = localDateTimeNow, - Action = LockerLogMessage.Add, - }; room.NumberOfLockers += 1; _context.Rooms.Update(room); - await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Locker), result.Entity.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index a9110258..9860aea9 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -70,19 +68,10 @@ public async Task Handle(Command request, CancellationToken cancellat var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - var log = new LockerLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = locker.Id, - Time = localDateTimeNow, - Action = LockerLogMessage.Remove, - }; var room = locker.Room; var result = _context.Lockers.Remove(locker); room.NumberOfLockers -= 1; _context.Rooms.Update(room); - await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Locker), locker.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 9826b345..45b5aed9 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; @@ -91,18 +89,7 @@ public async Task Handle(Command request, CancellationToken cancellat locker.LastModifiedBy = request.CurrentUser.Id; locker.IsAvailable = request.IsAvailable; - - - var log = new LockerLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = locker.Id, - Time = localDateTimeNow, - Action = LockerLogMessage.Update, - }; var result = _context.Lockers.Update(locker); - await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Locker), locker.Id, request.CurrentUser.Id)) diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index c17a3bd0..9dff5688 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -102,21 +100,11 @@ public async Task Handle(Command request, CancellationToken cancellatio var result = await _context.Rooms.AddAsync(entity, cancellationToken); - var log = new RoomLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = entity.Id, - Time = localDateTimeNow, - Action = RoomLogMessage.Add, - }; - using (Logging.PushProperties(nameof(Room), entity.Id, request.CurrentUser.Id)) { _logger.LogAddRoom(result.Entity.Id.ToString(), department.Name); } - await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index d4d6a1b2..4619d011 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -75,16 +73,6 @@ public async Task Handle(Command request, CancellationToken cancellatio var result = _context.Rooms.Remove(room); - var log = new RoomLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = room.Id, - Time = localDateTimeNow, - Action = RoomLogMessage.Remove, - }; - await _context.RoomLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Room), room.Id, request.CurrentUser.Id)) diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index 9c913ec7..b630cdaa 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; @@ -95,16 +93,6 @@ public async Task Handle(Command request, CancellationToken cancellatio _context.Rooms.Entry(room).State = EntityState.Modified; - var log = new RoomLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = room.Id, - Time = localDateTimeNow, - Action = RoomLogMessage.Update, - }; - - await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Room), room.Id, request.CurrentUser.Id)) diff --git a/src/Application/Staffs/Commands/AssignStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs index 1c8680b9..e8dd03ef 100644 --- a/src/Application/Staffs/Commands/AssignStaff.cs +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -90,17 +88,8 @@ public async Task Handle(Command request, CancellationToken cancellati room.LastModified = localDateTimeNow; room.LastModifiedBy = request.CurrentUser.Id; - var log = new UserLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = staff.User.Id, - Time = localDateTimeNow, - Action = UserLogMessages.Staff.AssignStaff, - }; _context.Rooms.Update(room); var result = _context.Staffs.Update(staff); - await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Staff), staff.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index 5e6ebfdd..c87aec9f 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,11 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -61,16 +59,7 @@ public async Task Handle(Command request, CancellationToken cancellati _context.Rooms.Update(staff.Room!); staff.Room = null; - var log = new UserLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = staff.User.Id, - Time = localDateTimeNow, - Action = UserLogMessages.Staff.RemoveFromRoom, - }; var result = _context.Staffs.Update(staff); - await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(User), result.Entity.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 52c07f0e..269c3d91 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,16 +1,12 @@ -using System.Runtime.CompilerServices; -using System.Text; using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Logging; -using Application.Common.Messages; using Application.Helpers; using Application.Identity; using Application.Users.Queries; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using Domain.Events; using FluentValidation; using MediatR; @@ -148,15 +144,6 @@ public async Task Handle(Command request, CancellationToken cancellatio } var result = await _context.Users.AddAsync(entity, cancellationToken); - var log = new UserLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = entity.Id, - Time = localDateTimeNow, - Action = UserLogMessages.Add, - }; - await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(User), entity.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 1b5f2254..805b0202 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -2,11 +2,9 @@ using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Messages; -using Application.Identity; using Application.Users.Queries; using AutoMapper; using Domain.Entities; -using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -85,16 +83,7 @@ public async Task Handle(Command request, CancellationToken cancellatio user.LastModified = localDateTimeNow; user.LastModifiedBy = request.CurrentUser.Id; - var log = new UserLog() - { - User = request.CurrentUser, - UserId = request.CurrentUser.Id, - ObjectId = user.Id, - Time = localDateTimeNow, - Action = UserLogMessages.Update, - }; var result = _context.Users.Update(user); - await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(User), result.Entity.Id, request.CurrentUser.Id)) { diff --git a/src/Domain/Entities/Logging/DocumentLog.cs b/src/Domain/Entities/Logging/DocumentLog.cs deleted file mode 100644 index e52fae0c..00000000 --- a/src/Domain/Entities/Logging/DocumentLog.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Domain.Common; -using Domain.Entities.Physical; - -namespace Domain.Entities.Logging; - -public class DocumentLog : BaseLoggingEntity -{ - public Folder? BaseFolder { get; set; } -} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/FolderLog.cs b/src/Domain/Entities/Logging/FolderLog.cs deleted file mode 100644 index e41cbdf2..00000000 --- a/src/Domain/Entities/Logging/FolderLog.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Domain.Common; -using Domain.Entities.Physical; - -namespace Domain.Entities.Logging; - -public class FolderLog : BaseLoggingEntity -{ - public Locker? BaseLocker { get; set; } -} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/LockerLog.cs b/src/Domain/Entities/Logging/LockerLog.cs deleted file mode 100644 index 96c8ea44..00000000 --- a/src/Domain/Entities/Logging/LockerLog.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Domain.Common; -using Domain.Entities.Physical; - -namespace Domain.Entities.Logging; - -public class LockerLog : BaseLoggingEntity -{ - public Room? BaseRoom { get; set; } -} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs deleted file mode 100644 index 5e801092..00000000 --- a/src/Domain/Entities/Logging/RequestLog.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Domain.Common; -using Domain.Entities.Physical; -using Domain.Enums; - -namespace Domain.Entities.Logging; - -public class RequestLog : BaseLoggingEntity -{ - public RequestType Type { get; set; } -} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RoomLog.cs b/src/Domain/Entities/Logging/RoomLog.cs deleted file mode 100644 index b662a46f..00000000 --- a/src/Domain/Entities/Logging/RoomLog.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Domain.Common; -using Domain.Entities.Physical; - -namespace Domain.Entities.Logging; - -public class RoomLog : BaseLoggingEntity -{ -} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/UserLog.cs b/src/Domain/Entities/Logging/UserLog.cs deleted file mode 100644 index 32735697..00000000 --- a/src/Domain/Entities/Logging/UserLog.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Domain.Common; - -namespace Domain.Entities.Logging; - -public class UserLog : BaseLoggingEntity -{ -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index e60d802a..15c4970e 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -3,7 +3,6 @@ using Application.Common.Models; using Domain.Entities; using Domain.Entities.Digital; -using Domain.Entities.Logging; using Domain.Entities.Physical; using Infrastructure.Common; using MediatR; @@ -40,14 +39,6 @@ public ApplicationDbContext( public DbSet ResetPasswordTokens => Set(); public DbSet Logs => Set(); - - public DbSet RoomLogs => Set(); - public DbSet LockerLogs => Set(); - public DbSet FolderLogs => Set(); - public DbSet DocumentLogs => Set(); - public DbSet RequestLogs => Set(); - public DbSet UserLogs => Set(); - protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs deleted file mode 100644 index 09f6db4c..00000000 --- a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Domain.Entities.Logging; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Infrastructure.Persistence.Configurations; - -public class UserLogConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(x => x.Id); - builder.Property(x => x.Id) - .ValueGeneratedOnAdd(); - - builder.HasOne(x => x.User) - .WithMany() - .HasForeignKey(x => x.UserId) - .IsRequired(); - } -} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.Designer.cs new file mode 100644 index 00000000..680ed43d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.Designer.cs @@ -0,0 +1,886 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230711074257_DeleteLogEntities")] + partial class DeleteLogEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeInBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsSharedRoot") + .HasColumnType("boolean"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.cs b/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.cs new file mode 100644 index 00000000..900c2b64 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230711074257_DeleteLogEntities.cs @@ -0,0 +1,228 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DeleteLogEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DocumentLogs"); + + migrationBuilder.DropTable( + name: "FolderLogs"); + + migrationBuilder.DropTable( + name: "LockerLogs"); + + migrationBuilder.DropTable( + name: "RequestLogs"); + + migrationBuilder.DropTable( + name: "RoomLogs"); + + migrationBuilder.DropTable( + name: "UserLogs"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DocumentLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BaseFolderId = table.Column(type: "uuid", nullable: true), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DocumentLogs", x => x.Id); + table.ForeignKey( + name: "FK_DocumentLogs_Folders_BaseFolderId", + column: x => x.BaseFolderId, + principalTable: "Folders", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_DocumentLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FolderLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BaseLockerId = table.Column(type: "uuid", nullable: true), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FolderLogs", x => x.Id); + table.ForeignKey( + name: "FK_FolderLogs_Lockers_BaseLockerId", + column: x => x.BaseLockerId, + principalTable: "Lockers", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_FolderLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LockerLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BaseRoomId = table.Column(type: "uuid", nullable: true), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LockerLogs", x => x.Id); + table.ForeignKey( + name: "FK_LockerLogs_Rooms_BaseRoomId", + column: x => x.BaseRoomId, + principalTable: "Rooms", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_LockerLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RequestLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false), + Type = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RequestLogs", x => x.Id); + table.ForeignKey( + name: "FK_RequestLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RoomLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomLogs", x => x.Id); + table.ForeignKey( + name: "FK_RoomLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogs", x => x.Id); + table.ForeignKey( + name: "FK_UserLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_BaseFolderId", + table: "DocumentLogs", + column: "BaseFolderId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_UserId", + table: "DocumentLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_BaseLockerId", + table: "FolderLogs", + column: "BaseLockerId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_UserId", + table: "FolderLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_BaseRoomId", + table: "LockerLogs", + column: "BaseRoomId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_UserId", + table: "LockerLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_UserId", + table: "RequestLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_UserId", + table: "RoomLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_UserId", + table: "UserLogs", + column: "UserId"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index eab106c3..fc480da7 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -175,180 +175,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Files"); }); - modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("BaseFolderId") - .HasColumnType("uuid"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("BaseFolderId"); - - b.HasIndex("UserId"); - - b.ToTable("DocumentLogs"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("BaseLockerId") - .HasColumnType("uuid"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("BaseLockerId"); - - b.HasIndex("UserId"); - - b.ToTable("FolderLogs"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("BaseRoomId") - .HasColumnType("uuid"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("BaseRoomId"); - - b.HasIndex("UserId"); - - b.ToTable("LockerLogs"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("RequestLogs"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("RoomLogs"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Action") - .IsRequired() - .HasColumnType("text"); - - b.Property("ObjectId") - .HasColumnType("uuid"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("UserLogs"); - }); - modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -865,90 +691,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Entry"); }); - modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => - { - b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") - .WithMany() - .HasForeignKey("BaseFolderId"); - - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("BaseFolder"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => - { - b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") - .WithMany() - .HasForeignKey("BaseLockerId"); - - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("BaseLocker"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => - { - b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") - .WithMany() - .HasForeignKey("BaseRoomId"); - - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("BaseRoom"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => - { - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => - { - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => - { - b.HasOne("Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") From 31e86fc03c1224f3738ca15bbe43df273dfb6a65 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:43:32 +0700 Subject: [PATCH 120/162] fix: delete shared download file (#367) --- src/Api/Controllers/SharedController.cs | 21 ------ .../Entries/Commands/DownloadDigitalFile.cs | 1 - .../Entries/Queries/DownloadSharedEntry.cs | 71 ------------------- 3 files changed, 93 deletions(-) delete mode 100644 src/Application/Entries/Queries/DownloadSharedEntry.cs diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 2c10fb15..d094598f 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -23,27 +23,6 @@ public SharedController(ICurrentUserService currentUserService) _currentUserService = currentUserService; } - /// - /// - /// - /// - /// - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("entries/{entryId:guid}/file")] - public async Task DownloadSharedFile([FromRoute] Guid entryId) - { - var currentUser = _currentUserService.GetCurrentUser(); - var query = new DownloadSharedEntry.Query() - { - CurrentUser = currentUser, - EntryId = entryId, - }; - var result = await Mediator.Send(query); - var content = new MemoryStream(result.FileData); - HttpContext.Response.ContentType = result.FileType; - return File(content, result.FileType, result.FileName); - } - /// /// /// diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 11715b84..5da05648 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -39,7 +39,6 @@ public async Task Handle(Command request, CancellationToken cancellation { var entry = await _context.Entries .Include(x => x.File) - .Include(x => x.Owner) .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); if (entry is null) diff --git a/src/Application/Entries/Queries/DownloadSharedEntry.cs b/src/Application/Entries/Queries/DownloadSharedEntry.cs deleted file mode 100644 index 2b9bf3bd..00000000 --- a/src/Application/Entries/Queries/DownloadSharedEntry.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Operations; -using Domain.Entities; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Entries.Queries; - -public class DownloadSharedEntry -{ - public record Query : IRequest - { - public User CurrentUser { get; init; } = null!; - public Guid EntryId { get; init; } - } - - public class Result - { - public string FileName { get; set; } = null!; - public string FileType { get; set; } = null!; - public byte[] FileData { get; set; } = null!; - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - public QueryHandler(IApplicationDbContext context) - { - _context = context; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var entry = await _context.Entries.FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); - - if (entry is null) - { - throw new KeyNotFoundException("Entry does not exist."); - } - - if (entry.IsDirectory) - { - throw new ConflictException("Entry cannot be downloaded."); - } - - if (entry.OwnerId != request.CurrentUser.Id) - { - var permission = await _context.EntryPermissions.FirstOrDefaultAsync( - x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, cancellationToken); - - if (permission is null || - !permission.AllowedOperations - .Split(",") - .Contains(EntryOperation.Download.ToString())) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } - } - - var result = new Result - { - FileName = $"{entry.Name}.{entry.File!.FileExtension}", - FileType = entry.File.FileType, - FileData = entry.File.FileData, - }; - - return result; - } - } -} \ No newline at end of file From ed20357e1a83bb1e2aa067c9bf811bde0065c402 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:40:20 +0700 Subject: [PATCH 121/162] fix: NotAllowedException to UnauthorizedAccessException (#368) * fix: NotAllowedException to UnauthorizedAccessException * stoopid --- src/Application/Entries/Commands/DeleteBinEntry.cs | 2 +- src/Application/Entries/Commands/MoveEntryToBin.cs | 2 +- src/Application/Entries/Commands/RestoreBinEntry.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Application/Entries/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs index 071f35c7..5b036bcf 100644 --- a/src/Application/Entries/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -57,7 +57,7 @@ public async Task Handle(Command request, CancellationToken cancellati if (!entry.Owner.Username.Equals(request.CurrentUser.Username)) { - throw new NotAllowedException("You do not have the permission to delete this entry."); + throw new UnauthorizedAccessException("You do not have the permission to delete this entry."); } if (entry.IsDirectory) diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index 287063df..4466c752 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -57,7 +57,7 @@ public async Task Handle(Command request, CancellationToken cancellati if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) { - throw new NotAllowedException("You do not have permission to move this entry into bin."); + throw new UnauthorizedAccessException("You do not have permission to move this entry into bin."); } var ownerUsername = entry.Owner.Username; diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index 14b7c16d..832c121a 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -57,7 +57,7 @@ public async Task Handle(Command request, CancellationToken cancellati if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) { - throw new NotAllowedException("You do not have the permission to restore this entry."); + throw new UnauthorizedAccessException("You do not have the permission to restore this entry."); } var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); From cb21d9816d7afe14720727d8c23f59a84c4f00f6 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Tue, 11 Jul 2023 19:27:05 +0700 Subject: [PATCH 122/162] fix: regex validation and change command name. (#366) * fix: regex validation and change command name. * refactoring --- src/Api/Controllers/EntriesController.cs | 8 ++++---- src/Api/Controllers/SharedController.cs | 6 +++--- .../{UploadDigitalFile.cs => CreateEntry.cs} | 13 +++++++------ .../{UploadSharedEntry.cs => CreateSharedEntry.cs} | 8 ++++---- src/Application/Entries/Commands/UpdateEntry.cs | 13 +++++++++++++ src/Application/Entries/EntryLogExtension.cs | 4 ++-- .../Entries/Queries/GetAllEntriesPaginated.cs | 2 +- 7 files changed, 34 insertions(+), 20 deletions(-) rename src/Application/Entries/Commands/{UploadDigitalFile.cs => CreateEntry.cs} (90%) rename src/Application/Entries/Commands/{UploadSharedEntry.cs => CreateSharedEntry.cs} (95%) diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 12ea545d..53556d74 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -29,7 +29,7 @@ public EntriesController(ICurrentUserService currentUserService) /// [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpPost] - public async Task>> UploadDigitalFile( + public async Task>> UploadEntry( [FromForm] UploadDigitalFileRequest request) { var currentUser = _currentUserService.GetCurrentUser(); @@ -50,11 +50,11 @@ public async Task>> UploadDigitalFile( }); } - UploadDigitalFile.Command command; + CreateEntry.Command command; if (request.IsDirectory) { - command = new UploadDigitalFile.Command() + command = new CreateEntry.Command() { CurrentUser = currentUser, Path = request.Path, @@ -72,7 +72,7 @@ public async Task>> UploadDigitalFile( var lastDotIndex = request.File.FileName.LastIndexOf(".", StringComparison.Ordinal); var extension = request.File.FileName.Substring(lastDotIndex + 1, request.File.FileName.Length - lastDotIndex - 1); - command = new UploadDigitalFile.Command() + command = new CreateEntry.Command() { CurrentUser = currentUser, Path = request.Path, diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index d094598f..146d20e9 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -78,11 +78,11 @@ public async Task>> UploadSharedEntry([ }); } - UploadSharedEntry.Command command; + CreateSharedEntry.Command command; if (request.IsDirectory) { - command = new UploadSharedEntry.Command() + command = new CreateSharedEntry.Command() { Name = request.Name, CurrentUser = currentUser, @@ -100,7 +100,7 @@ public async Task>> UploadSharedEntry([ var lastDotIndex = request.File.FileName.LastIndexOf(".", StringComparison.Ordinal); var extension = request.File.FileName.Substring(lastDotIndex + 1, request.File.FileName.Length - lastDotIndex - 1); - command = new UploadSharedEntry.Command() + command = new CreateSharedEntry.Command() { CurrentUser = currentUser, EntryId = entryId, diff --git a/src/Application/Entries/Commands/UploadDigitalFile.cs b/src/Application/Entries/Commands/CreateEntry.cs similarity index 90% rename from src/Application/Entries/Commands/UploadDigitalFile.cs rename to src/Application/Entries/Commands/CreateEntry.cs index eb6107c7..9de0cac3 100644 --- a/src/Application/Entries/Commands/UploadDigitalFile.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -14,7 +14,7 @@ namespace Application.Entries.Commands; -public class UploadDigitalFile { +public class CreateEntry { public class Validator : AbstractValidator { public Validator() @@ -22,11 +22,12 @@ public Validator() RuleLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.Name) + .NotEmpty().WithMessage("Entry's name is required.") .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); RuleFor(x => x.Path) - .NotEmpty().WithMessage("File's path is required.") - .Matches("^(/(?!/)[a-z_.\\-0-9]*)+(? private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - private readonly ILogger _logger; + private readonly ILogger _logger; - public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) + public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; @@ -122,7 +123,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entryEntity.Id, request.CurrentUser.Id)) { - _logger.LogUploadDigitalFile(request.CurrentUser.Id.ToString(), entryEntity.Id.ToString()); + _logger.LogCreateEntry(request.CurrentUser.Id.ToString(), entryEntity.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/UploadSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs similarity index 95% rename from src/Application/Entries/Commands/UploadSharedEntry.cs rename to src/Application/Entries/Commands/CreateSharedEntry.cs index 9e06a5f0..acdb78b3 100644 --- a/src/Application/Entries/Commands/UploadSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -14,7 +14,7 @@ namespace Application.Entries.Commands; -public class UploadSharedEntry +public class CreateSharedEntry { public class Validator : AbstractValidator { @@ -42,8 +42,8 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDateTimeProvider _dateTimeProvider; - private readonly ILogger _logger; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) + private readonly ILogger _logger; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider, ILogger logger) { _context = context; _mapper = mapper; @@ -129,7 +129,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entity.Id, request.CurrentUser.Id)) { - _logger.LogUploadSharedEntry(request.CurrentUser.Id.ToString(), entity.Id.ToString()); + _logger.LogCreateSharedEntry(request.CurrentUser.Id.ToString(), entity.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index 9341746f..b8318982 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Operations; using AutoMapper; using Domain.Entities.Digital; +using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -14,6 +15,18 @@ namespace Application.Entries.Commands; public class UpdateEntry { + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Entry's name is required.") + .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); + } + } + public record Command : IRequest { public Guid CurrentUserId { get; init; } diff --git a/src/Application/Entries/EntryLogExtension.cs b/src/Application/Entries/EntryLogExtension.cs index 28e4d2c1..3605ad4d 100644 --- a/src/Application/Entries/EntryLogExtension.cs +++ b/src/Application/Entries/EntryLogExtension.cs @@ -19,10 +19,10 @@ public static partial class EntryLogExtension public static partial void LogUpdateEntry(this ILogger logger, string userId, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadDigitalFile, EventId = EventId.Add)] - public static partial void LogUploadDigitalFile(this ILogger logger, string userId, string entryId); + public static partial void LogCreateEntry(this ILogger logger, string userId, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadSharedEntry, EventId = EventId.Add)] - public static partial void LogUploadSharedEntry(this ILogger logger, string userId, string entryId); + public static partial void LogCreateSharedEntry(this ILogger logger, string userId, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.DeleteBinEntry, EventId = EventId.Remove)] public static partial void LogDeleteBinEntry(this ILogger logger, string userId, string entryId); diff --git a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs index 1efa66fc..0c345df2 100644 --- a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs @@ -19,7 +19,7 @@ public Validator() RuleFor(x => x.EntryPath) .NotEmpty().WithMessage("File's path is required.") - .Matches("^(/(?!/)[a-z_.\\-0-9]*)+(? Date: Tue, 11 Jul 2023 20:28:00 +0700 Subject: [PATCH 123/162] fix regex (#372) --- src/Application/Entries/Commands/CreateEntry.cs | 2 +- src/Application/Entries/Queries/GetAllEntriesPaginated.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 9de0cac3..eeeb4deb 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -27,7 +27,7 @@ public Validator() RuleFor(x => x.Path) .NotEmpty().WithMessage("Entry's path is required.") - .Matches("^(/(?!/)[a-z_.\\s\\-0-9]*)+(? x.EntryPath) .NotEmpty().WithMessage("File's path is required.") - .Matches("^(/(?!/)[a-z_.\\s\\-0-9]*)+(? Date: Tue, 11 Jul 2023 21:29:55 +0700 Subject: [PATCH 124/162] Update/remove entry operations (#371) * update: entry permission now has only 2 permission operations * update: - expiry date is nullable now - fix some share entry logic --- src/Api/Controllers/EntriesController.cs | 4 +- .../Entries/ShareEntryPermissionRequest.cs | 6 +- .../Common/Messages/EntryLogMessages.cs | 2 +- .../Models/Dtos/Digital/EntryPermissionDto.cs | 14 +- .../Models/Operations/EntryOperation.cs | 4 +- .../Entries/Commands/CreateSharedEntry.cs | 2 +- .../Entries/Commands/DownloadDigitalFile.cs | 2 +- .../Entries/Commands/ShareEntry.cs | 45 +- .../Entries/Commands/UpdateEntry.cs | 2 +- .../Entries/Queries/DownloadSharedEntry.cs | 71 + .../Entries/Queries/GetPermissions.cs | 12 +- .../Entities/Digital/EntryPermission.cs | 2 +- .../EntryPermissionConfiguration.cs | 3 + ...missionExpiryDateNowIsNullable.Designer.cs | 1144 +++++++++++++++++ ..._EntryPermissionExpiryDateNowIsNullable.cs | 37 + .../ApplicationDbContextModelSnapshot.cs | 2 +- 16 files changed, 1293 insertions(+), 59 deletions(-) create mode 100644 src/Application/Entries/Queries/DownloadSharedEntry.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.cs diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 53556d74..c053dd91 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -169,9 +169,7 @@ public async Task>> ManagePermission( UserId = request.UserId, ExpiryDate = request.ExpiryDate, CanView = request.CanView, - CanUpload = request.CanUpload, - CanDownload = request.CanDownload, - CanChangePermission = request.CanChangePermission, + CanEdit = request.CanEdit, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs index ac50c522..98b64b2f 100644 --- a/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs @@ -3,9 +3,7 @@ namespace Api.Controllers.Payload.Requests.Entries; public class ShareEntryPermissionRequest { public Guid UserId { get; set; } - public DateTime ExpiryDate { get; set; } + public DateTime? ExpiryDate { get; set; } public bool CanView { get; set; } - public bool CanUpload { get; set; } - public bool CanDownload { get; set; } - public bool CanChangePermission { get; set; } + public bool CanEdit { get; set; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/EntryLogMessages.cs b/src/Application/Common/Messages/EntryLogMessages.cs index b03c8cda..79215184 100644 --- a/src/Application/Common/Messages/EntryLogMessages.cs +++ b/src/Application/Common/Messages/EntryLogMessages.cs @@ -6,7 +6,7 @@ public class EntryLogMessages public const string DownloadDigitalFile = "User with Id {UserId} download file with Id {FileId}"; public const string MoveEntryToBin = "User with Id {UserId} move entry with Id {EntryId} to bin"; public const string RestoreBinEntry = "User with Id {UserId} restore entry with Id {EntryId} from bin"; - public const string ShareEntry = "User with Id {UserId} share entry with Id {EntryId} to User with id {SharedUserId}"; + public const string ShareEntry = "User with Id {UserId} change permission of entry with Id {EntryId} to User with id {SharedUserId}"; public const string UpdateEntry = "User with Id {UserId} update entry with Id {EntryId}"; public const string UploadDigitalFile = "User with Id {UserId} upload an entry with id {EntryId}"; public const string UploadSharedEntry = "User with Id {UserId} upload an emtry with id {EntryId}"; diff --git a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs index 01c3a7e7..23e5e613 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs @@ -10,21 +10,15 @@ public class EntryPermissionDto : IMapFrom public Guid EmployeeId { get; set; } public Guid EntryId { get; set; } public bool CanView { get; set; } - public bool CanUpload { get; set; } - public bool CanDownload { get; set; } - public bool CanChangePermission { get; set; } + public bool CanEdit { get; set; } public bool IsSharedRoot { get; set; } public void Mapping(Profile profile) { profile.CreateMap() .ForMember(dest => dest.CanView, - opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.View.ToString()))) - .ForMember(dest => dest.CanUpload, - opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.Upload.ToString()))) - .ForMember(dest => dest.CanDownload, - opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.Download.ToString()))) - .ForMember(dest => dest.CanChangePermission, - opt => opt.MapFrom(src => src.AllowedOperations.Contains(EntryOperation.ChangePermission.ToString()))); + opt => opt.MapFrom(src => src.AllowedOperations.Split(',', StringSplitOptions.TrimEntries).Contains(EntryOperation.View.ToString()))) + .ForMember(dest => dest.CanEdit, + opt => opt.MapFrom(src => src.AllowedOperations.Split(',', StringSplitOptions.TrimEntries).Contains(EntryOperation.Edit.ToString()))); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Operations/EntryOperation.cs b/src/Application/Common/Models/Operations/EntryOperation.cs index f023ba29..48ebef4c 100644 --- a/src/Application/Common/Models/Operations/EntryOperation.cs +++ b/src/Application/Common/Models/Operations/EntryOperation.cs @@ -3,7 +3,5 @@ namespace Application.Common.Models.Operations; public enum EntryOperation { View, - Upload, - Download, - ChangePermission + Edit, } \ No newline at end of file diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index acdb78b3..057b2627 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -59,7 +59,7 @@ public async Task Handle(Command request, CancellationToken cancellati .Include(x => x.Entry) .ThenInclude(x => x.Owner) .Where(x => x.EmployeeId == request.CurrentUser.Id - && x.AllowedOperations.Contains(EntryOperation.Upload.ToString())); + && x.AllowedOperations.Contains(EntryOperation.Edit.ToString())); var rootEntry = await permissions .Select(x => x.Entry) diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 5da05648..70cc4488 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -60,7 +60,7 @@ public async Task Handle(Command request, CancellationToken cancellation if (permission is null || !permission.AllowedOperations .Split(",") - .Contains(EntryOperation.Download.ToString())) + .Contains(EntryOperation.View.ToString())) { throw new UnauthorizedAccessException("User cannot access this resource."); } diff --git a/src/Application/Entries/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs index 099100d4..14464b87 100644 --- a/src/Application/Entries/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -21,11 +21,9 @@ public record Command : IRequest public User CurrentUser { get; init; } = null!; public Guid EntryId { get; init; } public Guid UserId { get; init; } - public DateTime ExpiryDate { get; init; } + public DateTime? ExpiryDate { get; init; } public bool CanView { get; init; } - public bool CanUpload { get; init; } - public bool CanDownload { get; init; } - public bool CanChangePermission { get; init; } + public bool CanEdit { get; init; } } public class CommandHandler : IRequestHandler @@ -56,7 +54,7 @@ public async Task Handle(Command request, CancellationToken var canChangeEntryPermission = _context.EntryPermissions.Any(x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id - && x.AllowedOperations.Contains(EntryOperation.ChangePermission.ToString())); + && x.AllowedOperations.Contains(EntryOperation.Edit.ToString())); if (entry.OwnerId != request.CurrentUser.Id && !canChangeEntryPermission) { @@ -80,9 +78,15 @@ public async Task Handle(Command request, CancellationToken throw new ConflictException("Expiry date cannot be in the past."); } - var allowOperations = GenerateAllowOperations(request, entry.IsDirectory); + var allowOperations = GenerateAllowOperations(request); + + + var isShareRoot = !await _context.EntryPermissions + .AnyAsync(x => (x.Entry.Path == "/" ? x.Entry.Path + x.Entry.Name : x.Entry.Path + "/" + x.Entry.Name) == entry.Path + && x.EmployeeId == request.UserId + && x.Entry.FileId == null, cancellationToken); - await GrantOrRevokePermission(entry, user, allowOperations, request.ExpiryDate, true, cancellationToken); + await GrantOrRevokePermission(entry, user, allowOperations, request.ExpiryDate, isShareRoot, cancellationToken); if (entry.IsDirectory) { @@ -95,7 +99,7 @@ public async Task Handle(Command request, CancellationToken .ToList(); foreach (var childEntry in childEntries) { - var childAllowOperations = GenerateAllowOperations(request, childEntry.IsDirectory); + var childAllowOperations = GenerateAllowOperations(request); await GrantOrRevokePermission(childEntry, user, childAllowOperations, request.ExpiryDate, false, cancellationToken); } } @@ -114,7 +118,7 @@ private async Task GrantOrRevokePermission( Entry entry, User user, string allowOperations, - DateTime expiryDate, + DateTime? expiryDate, bool isSharedRoot, CancellationToken cancellationToken) { @@ -124,31 +128,32 @@ private async Task GrantOrRevokePermission( if (existedPermission is null) { + if (string.IsNullOrEmpty(allowOperations)) return; var entryPermission = new EntryPermission { EmployeeId = user.Id, EntryId = entry.Id, AllowedOperations = allowOperations, - ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate), + ExpiryDateTime = expiryDate is null ? null : LocalDateTime.FromDateTime(expiryDate.Value), IsSharedRoot = isSharedRoot, Employee = user, Entry = entry, }; await _context.EntryPermissions.AddAsync(entryPermission, cancellationToken); } - else if (allowOperations.Equals(string.Empty)) + else if (string.IsNullOrEmpty(allowOperations)) { _context.EntryPermissions.Remove(existedPermission); } else { existedPermission.AllowedOperations = allowOperations; - existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); + existedPermission.ExpiryDateTime = expiryDate is null ? null : LocalDateTime.FromDateTime(expiryDate.Value); _context.EntryPermissions.Update(existedPermission); } } - private static string GenerateAllowOperations(Command request, bool isDirectory) + private static string GenerateAllowOperations(Command request) { var allowOperations = new CommaDelimitedStringCollection(); @@ -161,19 +166,9 @@ private static string GenerateAllowOperations(Command request, bool isDirectory) return string.Empty; } - if (request is { CanView: true, CanUpload: true } && isDirectory) - { - allowOperations.Add(EntryOperation.Upload.ToString()); - } - - if (request is { CanView: true, CanDownload: true } && !isDirectory) - { - allowOperations.Add(EntryOperation.Download.ToString()); - } - - if (request is { CanView: true, CanChangePermission: true }) + if (request is { CanView: true, CanEdit: true }) { - allowOperations.Add(EntryOperation.ChangePermission.ToString()); + allowOperations.Add(EntryOperation.Edit.ToString()); } return allowOperations.ToString(); diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index b8318982..04dde216 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -69,7 +69,7 @@ public async Task Handle(Command request, CancellationToken cancellati if (permission is null || !permission.AllowedOperations .Split(",") - .Contains(EntryOperation.Upload.ToString())) + .Contains(EntryOperation.Edit.ToString())) { throw new UnauthorizedAccessException("User cannot access this resource."); } diff --git a/src/Application/Entries/Queries/DownloadSharedEntry.cs b/src/Application/Entries/Queries/DownloadSharedEntry.cs new file mode 100644 index 00000000..eac78b6f --- /dev/null +++ b/src/Application/Entries/Queries/DownloadSharedEntry.cs @@ -0,0 +1,71 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Operations; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class DownloadSharedEntry +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Result + { + public string FileName { get; set; } = null!; + public string FileType { get; set; } = null!; + public byte[] FileData { get; set; } = null!; + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries.FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + if (entry.IsDirectory) + { + throw new ConflictException("Entry cannot be downloaded."); + } + + if (entry.OwnerId != request.CurrentUser.Id) + { + var permission = await _context.EntryPermissions.FirstOrDefaultAsync( + x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, cancellationToken); + + if (permission is null || + !permission.AllowedOperations + .Split(",") + .Contains(EntryOperation.View.ToString())) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + + var result = new Result + { + FileName = $"{entry.Name}.{entry.File!.FileExtension}", + FileType = entry.File.FileType, + FileData = entry.File.FileData, + }; + + return result; + } + } +} \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetPermissions.cs b/src/Application/Entries/Queries/GetPermissions.cs index 75c663c4..17aaa82e 100644 --- a/src/Application/Entries/Queries/GetPermissions.cs +++ b/src/Application/Entries/Queries/GetPermissions.cs @@ -42,8 +42,7 @@ public async Task Handle(Query request, CancellationToken ca if (entry.OwnerId == request.CurrentUser.Id) { return CreateEntryPermissionDto(entry.Id, entry.OwnerId, - true, true, true, true, - false); + true, true, false); } var permission = await _context.EntryPermissions @@ -54,8 +53,7 @@ public async Task Handle(Query request, CancellationToken ca if (permission is null) { return CreateEntryPermissionDto(entry.Id, request.CurrentUser.Id, - false, false, false, false, - false); + false, false, false); } return _mapper.Map(permission); @@ -63,7 +61,7 @@ public async Task Handle(Query request, CancellationToken ca private static EntryPermissionDto CreateEntryPermissionDto( Guid entryId, Guid userId, - bool canView, bool canUpload, bool canDownload, bool canChangePermission, + bool canView, bool canEdit, bool isSharedRoot) { return new EntryPermissionDto @@ -71,9 +69,7 @@ private static EntryPermissionDto CreateEntryPermissionDto( EmployeeId = userId, EntryId = entryId, CanView = canView, - CanUpload = canUpload, - CanDownload = canDownload, - CanChangePermission = canChangePermission, + CanEdit = canEdit, IsSharedRoot = isSharedRoot, }; } diff --git a/src/Domain/Entities/Digital/EntryPermission.cs b/src/Domain/Entities/Digital/EntryPermission.cs index cb6a624f..2949b379 100644 --- a/src/Domain/Entities/Digital/EntryPermission.cs +++ b/src/Domain/Entities/Digital/EntryPermission.cs @@ -7,7 +7,7 @@ public class EntryPermission public Guid EmployeeId { get; set; } public Guid EntryId { get; set; } public string AllowedOperations { get; set; } = null!; - public LocalDateTime ExpiryDateTime { get; set; } + public LocalDateTime? ExpiryDateTime { get; set; } public bool IsSharedRoot { get; set; } public User Employee { get; set; } = null!; diff --git a/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs index b343f08d..6c952d5b 100644 --- a/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/EntryPermissionConfiguration.cs @@ -15,5 +15,8 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.IsSharedRoot) .IsRequired(); + + builder.Property(x => x.ExpiryDateTime) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.Designer.cs new file mode 100644 index 00000000..342f941d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.Designer.cs @@ -0,0 +1,1144 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230711090539_EntryPermissionExpiryDateNowIsNullable")] + partial class EntryPermissionExpiryDateNowIsNullable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeInBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsSharedRoot") + .HasColumnType("boolean"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.cs b/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.cs new file mode 100644 index 00000000..c9133eb5 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230711090539_EntryPermissionExpiryDateNowIsNullable.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class EntryPermissionExpiryDateNowIsNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExpiryDateTime", + table: "EntryPermissions", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(LocalDateTime), + oldType: "timestamp without time zone"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExpiryDateTime", + table: "EntryPermissions", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0), + oldClrType: typeof(LocalDateTime), + oldType: "timestamp without time zone", + oldNullable: true); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index fc480da7..eaef20a9 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -139,7 +139,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("ExpiryDateTime") + b.Property("ExpiryDateTime") .HasColumnType("timestamp without time zone"); b.Property("IsSharedRoot") From b5a163ad59eb72fc203a1989ec2a000beb44a060 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Tue, 11 Jul 2023 21:47:49 +0700 Subject: [PATCH 125/162] feat/get bin entries and get bin entry by id (#369) * feat: implement get bin entries by id and all paginated * refactoring * refactoring * exception refactoring --- src/Api/Controllers/BinController.cs | 52 +++++++++++++++- src/Api/Controllers/EntriesController.cs | 1 - ...etAllBinEntriesPaginatedQueryParameters.cs | 6 ++ .../Queries/GetAllBinEntriesPaginated.cs | 62 +++++++++++++++++++ .../Entries/Queries/GetBinEntryById.cs | 59 ++++++++++++++++++ 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs create mode 100644 src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs create mode 100644 src/Application/Entries/Queries/GetBinEntryById.cs diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index 81d37689..3401b494 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -1,6 +1,8 @@ -using Application.Common.Interfaces; +using Api.Controllers.Payload.Requests.BinEntries; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Digital; +using Application.Entries.Queries; using Application.Entries.Commands; using Application.Identity; using Infrastructure.Identity.Authorization; @@ -82,4 +84,52 @@ public async Task>> DeleteBinEntry( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId:guid}")] + public async Task>> GetBinEntryById( + [FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + + var command = new GetBinEntryById.Query() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries")] + public async Task>> GetAllBinEntriesPaginated( + [FromQuery] GetAllBinEntriesPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + + var command = new GetAllBinEntriesPaginated.Query() + { + CurrentUser = currentUser, + Page = queryParameters.Page, + Size = queryParameters.Size, + SearchTerm = queryParameters.SearchTerm, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + + var result = await Mediator.Send(command); + return Ok(Result>.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index c053dd91..6b7b4f51 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -199,5 +199,4 @@ public async Task DownloadFile([FromRoute] Guid entryId) HttpContext.Response.ContentType = result.FileType; return File(result.Content, result.FileType, result.FileName); } - } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs new file mode 100644 index 00000000..9586b72a --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.BinEntries; + +public class GetAllBinEntriesPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs new file mode 100644 index 00000000..2c21f199 --- /dev/null +++ b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs @@ -0,0 +1,62 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class GetAllBinEntriesPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var pattern = $"{request.CurrentUser.Username}_bin/%"; + + var entries = _context.Entries + .Include(x => x.Owner) + .AsQueryable() + .Where(x => x.Owner.Username.Equals(request.CurrentUser.Username) + && EF.Functions.Like(x.Path, pattern)); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + entries = entries.Where(x => + x.Name.Contains(request.SearchTerm.Trim())); + } + + return await entries + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetBinEntryById.cs b/src/Application/Entries/Queries/GetBinEntryById.cs new file mode 100644 index 00000000..df2e831d --- /dev/null +++ b/src/Application/Entries/Queries/GetBinEntryById.cs @@ -0,0 +1,59 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Digital; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Entries.Queries; + +public class GetBinEntryById +{ + public record Query : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid EntryId { get; init; } + } + + public class Handler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IMapper mapper, IApplicationDbContext context) + { + _mapper = mapper; + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var entry = await _context.Entries + .Include(x => x.Owner) + .Include(x => x.Uploader) + .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); + + if (entry is null) + { + throw new KeyNotFoundException("Entry does not exist."); + } + + var entryPath = entry.Path; + var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); + var binCheck = entryPath.Substring(0, firstSlashIndex); + + if (!binCheck.Contains("_bin")) + { + throw new ConflictException("Entry is not in bin."); + } + + if (!entry.Owner.Username.Equals(request.CurrentUser.Username)) + { + throw new UnauthorizedAccessException("You do not have permission to view this entry"); + } + + return _mapper.Map(entry); + } + } +} \ No newline at end of file From 3ce3cf88fb44a9cfd981ab8d69d6280f086b9ea9 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 11 Jul 2023 22:54:49 +0700 Subject: [PATCH 126/162] include file (#374) --- src/Application/Entries/Queries/GetEntryById.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Application/Entries/Queries/GetEntryById.cs b/src/Application/Entries/Queries/GetEntryById.cs index e46b9b97..0a64c880 100644 --- a/src/Application/Entries/Queries/GetEntryById.cs +++ b/src/Application/Entries/Queries/GetEntryById.cs @@ -27,6 +27,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { var entry = await _context.Entries + .Include(x => x.File) .Include(x => x.Uploader) .ThenInclude(x => x.Department) .Include(x => x.Owner) From a70a435b1bcc6f35bfb73c1b9cd510ea4f814eac Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Tue, 11 Jul 2023 23:13:37 +0700 Subject: [PATCH 127/162] fix (#375) * fix * oops --- .../Queries/GetAllSharedUsersOfASharedEntryPaginated.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs index a8f02bd0..330b2a42 100644 --- a/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs +++ b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs @@ -46,9 +46,10 @@ public async Task> Handle(Query request, CancellationToke .FirstOrDefaultAsync(x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, cancellationToken); - if (permission is null || !permission.AllowedOperations.Contains(EntryOperation.View.ToString())) + if ((permission is null || !permission.AllowedOperations.Contains(EntryOperation.View.ToString())) && + (entry.OwnerId != request.CurrentUser.Id)) { - throw new NotAllowedException("You do not have permission to view this shared entry's users."); + throw new UnauthorizedAccessException("You do not have permission to view this shared entry's users."); } var sharedUsers = _context.EntryPermissions From 18086eae15f8c11e4fa4cae2ebe11072c40a83db Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 11 Jul 2023 23:15:36 +0700 Subject: [PATCH 128/162] fix (#377) --- src/Api/Controllers/SharedController.cs | 4 +- .../Models/Dtos/Digital/EntryPermissionDto.cs | 4 ++ ...etAllSharedUsersOfASharedEntryPaginated.cs | 38 ++++++++++--------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 146d20e9..9fe24165 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -121,7 +121,7 @@ public async Task>> UploadSharedEntry([ /// [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId:guid}/shared-users")] - public async Task>> GetSharedUsersFromASharedEntryPaginated( + public async Task>> GetSharedUsersFromASharedEntryPaginated( [FromRoute] Guid entryId, [FromQuery] GetAllSharedUsersFromASharedEntryPaginatedQueryParameters queryParameters) { @@ -135,7 +135,7 @@ public async Task>> GetSharedUsersFromAShare EntryId = entryId, }; var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); + return Ok(Result>.Succeed(result)); } /// diff --git a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs index 23e5e613..9165b3c5 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryPermissionDto.cs @@ -1,6 +1,8 @@ using Application.Common.Mappings; using Application.Common.Models.Operations; +using Application.Users.Queries; using AutoMapper; +using Domain.Entities; using Domain.Entities.Digital; namespace Application.Common.Models.Dtos.Digital; @@ -13,6 +15,8 @@ public class EntryPermissionDto : IMapFrom public bool CanEdit { get; set; } public bool IsSharedRoot { get; set; } + public UserDto Employee { get; set; } = null!; + public void Mapping(Profile profile) { profile.CreateMap() diff --git a/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs index 330b2a42..29d86754 100644 --- a/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs +++ b/src/Application/Users/Queries/GetAllSharedUsersOfASharedEntryPaginated.cs @@ -2,9 +2,11 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; using MediatR; using Microsoft.EntityFrameworkCore; @@ -12,7 +14,7 @@ namespace Application.Users.Queries; public class GetAllSharedUsersOfASharedEntryPaginated { - public record Query : IRequest> + public record Query : IRequest> { public User CurrentUser { get; init; } = null!; public Guid EntryId { get; init; } @@ -21,7 +23,7 @@ public record Query : IRequest> public int? Size { get; init; } } - public class Handler : IRequestHandler> + public class Handler : IRequestHandler> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -32,7 +34,7 @@ public Handler(IMapper mapper, IApplicationDbContext context) _context = context; } - public async Task> Handle(Query request, CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { var entry = await _context.Entries .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); @@ -51,25 +53,27 @@ public async Task> Handle(Query request, CancellationToke { throw new UnauthorizedAccessException("You do not have permission to view this shared entry's users."); } - - var sharedUsers = _context.EntryPermissions - .Where(x => x.EntryId == request.EntryId) - .Select(x => x.Employee); + + var permissions = _context.EntryPermissions + .Include(x => x.Employee) + .Where(x => x.EntryId == request.EntryId); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { - sharedUsers = sharedUsers.Where(x => - x.Username.ToLower().Trim().Contains(request.SearchTerm.ToLower().Trim())); + permissions = permissions.Where(x => + x.Employee.Username.ToLower().Trim().Contains(request.SearchTerm.ToLower().Trim())); } - return await sharedUsers - .ListPaginateWithSortAsync( - request.Page, - request.Size, - null, - null, - _mapper.ConfigurationProvider, - cancellationToken); + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 10 : request.Size; + + var count = await permissions.CountAsync(cancellationToken); + var list = await permissions + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file From cacafa5c80ecfe9973e16cb7acc9b9e5a9f8db8b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:39:41 +0700 Subject: [PATCH 129/162] update entry now update its child too (#378) --- .../Entries/Commands/UpdateEntry.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index 04dde216..70982549 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -87,11 +87,40 @@ public async Task Handle(Command request, CancellationToken cancellati var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var oldPath = entry.Path.Equals("/") ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + entry.Name = request.Name; entry.LastModified = localDateTimeNow; entry.LastModifiedBy = request.CurrentUserId; var result = _context.Entries.Update(entry); + + if (entry.IsDirectory) + { + // Update permissions for child Entries + var newPath = entry.Path.Equals("/") ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var pattern = $"{oldPath}/%"; + var childEntries = _context.Entries + .Where(x => (x.Path.Equals(oldPath) || EF.Functions.Like(x.Path, pattern)) + && x.OwnerId == entry.OwnerId) + .ToList(); + var count = oldPath.Length; + foreach (var childEntry in childEntries) + { + if (childEntry.Path.Equals(oldPath)) + { + childEntry.Path = newPath; + } + else + { + var after = childEntry.Path[(count+1)..]; + childEntry.Path = $"{newPath}/{after}"; + } + } + + _context.Entries.UpdateRange(childEntries); + } + await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUserId)) { From e2c1a1891d0ff587ad08e4cba249f10c179eddd3 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:47:13 +0700 Subject: [PATCH 130/162] update: create shared entries now have permission checked (#376) --- src/Api/Controllers/SharedController.cs | 2 +- .../Entries/Commands/CreateSharedEntry.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 9fe24165..a7e0945a 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -57,7 +57,7 @@ public async Task>> GetAll( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> UploadSharedEntry([ + public async Task>> CreateSharedEntry([ FromRoute] Guid entryId, [FromForm] UploadSharedEntryRequest request) { diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index 057b2627..48c55d8b 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -58,7 +58,7 @@ public async Task Handle(Command request, CancellationToken cancellati .ThenInclude(y => y.Uploader) .Include(x => x.Entry) .ThenInclude(x => x.Owner) - .Where(x => x.EmployeeId == request.CurrentUser.Id + .Where(x => x.EmployeeId == request.CurrentUser.Id && x.AllowedOperations.Contains(EntryOperation.Edit.ToString())); var rootEntry = await permissions @@ -74,7 +74,17 @@ public async Task Handle(Command request, CancellationToken cancellati { throw new ConflictException("This is a file."); } - + + var permission = _context.EntryPermissions.FirstOrDefaultAsync( + x => x.EntryId == rootEntry.Id && + x.EmployeeId == request.CurrentUser.Id && + x.AllowedOperations.Contains(EntryOperation.Edit.ToString()), cancellationToken); + + if (permission is null) + { + throw new UnauthorizedAccessException("User is not allowed to create an entry."); + } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var entryPath = rootEntry.Path + "/" + request.Name.Trim(); From 4da1da0a16404b6c7e688caa8d1f33754f575e16 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:48:09 +0700 Subject: [PATCH 131/162] fix: log entries (#373) --- src/Api/Api.csproj | 4 ---- src/Api/Controllers/EntriesController.cs | 8 ++++---- .../Common/Messages/EntryLogMessages.cs | 16 ++++++++-------- src/Application/Entries/Commands/CreateEntry.cs | 2 +- .../Entries/Commands/CreateSharedEntry.cs | 2 +- .../Entries/Commands/DeleteBinEntry.cs | 2 +- .../Entries/Commands/DownloadDigitalFile.cs | 11 ++++++----- .../Entries/Commands/MoveEntryToBin.cs | 2 +- .../Entries/Commands/RestoreBinEntry.cs | 2 +- src/Application/Entries/Commands/ShareEntry.cs | 2 +- src/Application/Entries/Commands/UpdateEntry.cs | 13 +++++++------ src/Application/Entries/EntryLogExtension.cs | 16 ++++++++-------- 12 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 4505ce9f..d45ec98c 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -24,8 +24,4 @@ - - - - diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 6b7b4f51..b088f822 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -98,12 +98,12 @@ public async Task>> Update( [FromRoute] Guid entryId, [FromBody] UpdateEntryRequest request) { - var currentUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateEntry.Command() { Name = request.Name, EntryId = entryId, - CurrentUserId = currentUserId + CurrentUser = currentUser }; var result = await Mediator.Send(command); @@ -188,10 +188,10 @@ public async Task>> ManagePermission( [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task DownloadFile([FromRoute] Guid entryId) { - var currentUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new DownloadDigitalFile.Command() { - CurrentUserId = currentUserId, + CurrentUser = currentUser, EntryId = entryId }; diff --git a/src/Application/Common/Messages/EntryLogMessages.cs b/src/Application/Common/Messages/EntryLogMessages.cs index 79215184..19bc5a77 100644 --- a/src/Application/Common/Messages/EntryLogMessages.cs +++ b/src/Application/Common/Messages/EntryLogMessages.cs @@ -2,12 +2,12 @@ public class EntryLogMessages { - public const string DeleteBinEntry = "User with Id {UserId} delete entry with Id {EntryId}"; - public const string DownloadDigitalFile = "User with Id {UserId} download file with Id {FileId}"; - public const string MoveEntryToBin = "User with Id {UserId} move entry with Id {EntryId} to bin"; - public const string RestoreBinEntry = "User with Id {UserId} restore entry with Id {EntryId} from bin"; - public const string ShareEntry = "User with Id {UserId} change permission of entry with Id {EntryId} to User with id {SharedUserId}"; - public const string UpdateEntry = "User with Id {UserId} update entry with Id {EntryId}"; - public const string UploadDigitalFile = "User with Id {UserId} upload an entry with id {EntryId}"; - public const string UploadSharedEntry = "User with Id {UserId} upload an emtry with id {EntryId}"; + public const string DeleteBinEntry = "User with username {Username} delete entry with Id {EntryId}"; + public const string DownloadDigitalFile = "User with username {Username} download file with Id {FileId}"; + public const string MoveEntryToBin = "User with username {Username} move entry with Id {EntryId} to bin"; + public const string RestoreBinEntry = "User with username {Username} restore entry with Id {EntryId} from bin"; + public const string ShareEntry = "User with username {Username} change permission of entry with Id {EntryId} to User with id {SharedUsername}"; + public const string UpdateEntry = "User with username {Username} update entry with Id {EntryId}"; + public const string UploadDigitalFile = "User with username {Username} upload an entry with id {EntryId}"; + public const string UploadSharedEntry = "User with username {Username} upload an emtry with id {EntryId}"; } \ No newline at end of file diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index eeeb4deb..e5dcc8f8 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -123,7 +123,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entryEntity.Id, request.CurrentUser.Id)) { - _logger.LogCreateEntry(request.CurrentUser.Id.ToString(), entryEntity.Id.ToString()); + _logger.LogCreateEntry(request.CurrentUser.Username, entryEntity.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index 48c55d8b..59188e5e 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -139,7 +139,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entity.Id, request.CurrentUser.Id)) { - _logger.LogCreateSharedEntry(request.CurrentUser.Id.ToString(), entity.Id.ToString()); + _logger.LogCreateSharedEntry(request.CurrentUser.Username, entity.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs index 5b036bcf..0b0ada6b 100644 --- a/src/Application/Entries/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -78,7 +78,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogDeleteBinEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + _logger.LogDeleteBinEntry(request.CurrentUser.Username, entry.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 70cc4488..8ea8fad1 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -2,6 +2,7 @@ using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Operations; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -12,7 +13,7 @@ public class DownloadDigitalFile { public record Command : IRequest { - public Guid CurrentUserId { get; init;} + public User CurrentUser { get; init; } = null!; public Guid EntryId { get; init; } } @@ -51,11 +52,11 @@ public async Task Handle(Command request, CancellationToken cancellation throw new ConflictException("Can not download folder."); } - if (entry.OwnerId != request.CurrentUserId) + if (entry.OwnerId != request.CurrentUser.Id) { var permission = await _context.EntryPermissions.FirstOrDefaultAsync( x => x.EntryId == request.EntryId - && x.EmployeeId == request.CurrentUserId, cancellationToken); + && x.EmployeeId == request.CurrentUser.Id, cancellationToken); if (permission is null || !permission.AllowedOperations @@ -71,9 +72,9 @@ public async Task Handle(Command request, CancellationToken cancellation var fileExtension = entry.File!.FileExtension; var fileId = entry.File!.Id; - using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUserId)) + using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogDownLoadFile(request.CurrentUserId.ToString(), fileId.ToString()); + _logger.LogDownLoadFile(request.CurrentUser.Username, fileId.ToString()); } return new Result() diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index 4466c752..d49242b6 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -91,7 +91,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogMoveEntryToBin(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + _logger.LogMoveEntryToBin(request.CurrentUser.Username, entry.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index 832c121a..03f0ea54 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -134,7 +134,7 @@ public async Task Handle(Command request, CancellationToken cancellati await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogRestoreBinEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString()); + _logger.LogRestoreBinEntry(request.CurrentUser.Username, entry.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs index 14464b87..320454b6 100644 --- a/src/Application/Entries/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -106,7 +106,7 @@ public async Task Handle(Command request, CancellationToken await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogShareEntry(request.CurrentUser.Id.ToString(), entry.Id.ToString(), request.UserId.ToString()); + _logger.LogShareEntry(request.CurrentUser.Username, entry.Id.ToString(), user.Username); } var result = await _context.EntryPermissions.FirstOrDefaultAsync(x => x.EntryId == request.EntryId diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index 70982549..7f463cb7 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -4,6 +4,7 @@ using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Operations; using AutoMapper; +using Domain.Entities; using Domain.Entities.Digital; using FluentValidation; using MediatR; @@ -29,7 +30,7 @@ public Validator() public record Command : IRequest { - public Guid CurrentUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid EntryId { get; init; } public string Name { get; init; } = null!; } @@ -60,11 +61,11 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("Entry does not exist."); } - if (entry.OwnerId != request.CurrentUserId) + if (entry.OwnerId != request.CurrentUser.Id) { var permission = await _context.EntryPermissions.FirstOrDefaultAsync( x => x.EntryId == request.EntryId - && x.EmployeeId == request.CurrentUserId, cancellationToken); + && x.EmployeeId == request.CurrentUser.Id, cancellationToken); if (permission is null || !permission.AllowedOperations @@ -91,7 +92,7 @@ public async Task Handle(Command request, CancellationToken cancellati entry.Name = request.Name; entry.LastModified = localDateTimeNow; - entry.LastModifiedBy = request.CurrentUserId; + entry.LastModifiedBy = request.CurrentUser.Id; var result = _context.Entries.Update(entry); @@ -122,9 +123,9 @@ public async Task Handle(Command request, CancellationToken cancellati } await _context.SaveChangesAsync(cancellationToken); - using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUserId)) + using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) { - _logger.LogUpdateEntry(request.CurrentUserId.ToString(), entry.Id.ToString()); + _logger.LogUpdateEntry(request.CurrentUser.Username, entry.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/EntryLogExtension.cs b/src/Application/Entries/EntryLogExtension.cs index 3605ad4d..e2f850b4 100644 --- a/src/Application/Entries/EntryLogExtension.cs +++ b/src/Application/Entries/EntryLogExtension.cs @@ -7,27 +7,27 @@ namespace Application.Entries; public static partial class EntryLogExtension { [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.DownloadDigitalFile, EventId = Common.Logging.EventId.Other)] - public static partial void LogDownLoadFile(this ILogger logger, string userId, string fileId); + public static partial void LogDownLoadFile(this ILogger logger, string username, string fileId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.MoveEntryToBin, EventId = Common.Logging.EventId.Update)] - public static partial void LogMoveEntryToBin(this ILogger logger, string userId, string entryId); + public static partial void LogMoveEntryToBin(this ILogger logger, string username, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.ShareEntry, EventId = EventId.OtherRequestRelated)] - public static partial void LogShareEntry(this ILogger logger, string userId, string entryId, string sharedUserId); + public static partial void LogShareEntry(this ILogger logger, string username, string entryId, string sharedUsername); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UpdateEntry, EventId = EventId.Update)] - public static partial void LogUpdateEntry(this ILogger logger, string userId, string entryId); + public static partial void LogUpdateEntry(this ILogger logger, string username, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadDigitalFile, EventId = EventId.Add)] - public static partial void LogCreateEntry(this ILogger logger, string userId, string entryId); + public static partial void LogCreateEntry(this ILogger logger, string username, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.UploadSharedEntry, EventId = EventId.Add)] - public static partial void LogCreateSharedEntry(this ILogger logger, string userId, string entryId); + public static partial void LogCreateSharedEntry(this ILogger logger, string username, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.DeleteBinEntry, EventId = EventId.Remove)] - public static partial void LogDeleteBinEntry(this ILogger logger, string userId, string entryId); + public static partial void LogDeleteBinEntry(this ILogger logger, string username, string entryId); [LoggerMessage(Level = LogLevel.Information, Message = EntryLogMessages.RestoreBinEntry, EventId = EventId.Update)] - public static partial void LogRestoreBinEntry(this ILogger logger, string userId, string entryId); + public static partial void LogRestoreBinEntry(this ILogger logger, string username, string entryId); } From 95b21474b9fc6b814af303fab02e267c41f08278 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:48:51 +0700 Subject: [PATCH 132/162] refactor: add entries api documentation (#365) * refactor: add entries api documentation * fix: something * done * Update BinController.cs --------- Co-authored-by: StarryFolf <67864500+StarryFolf@users.noreply.github.com> --- src/Api/Controllers/BinController.cs | 32 +++++++++-------- src/Api/Controllers/EntriesController.cs | 25 ++++++++++---- src/Api/Controllers/SharedController.cs | 44 ++++++++++++++++++------ 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index 3401b494..3b7cd3e2 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -20,10 +20,10 @@ public BinController(ICurrentUserService currentUserService) } /// - /// Delete an entry + /// Move an entry to bin /// /// - /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpPost("entries")] public async Task>> MoveEntryToBin( @@ -42,10 +42,10 @@ public async Task>> MoveEntryToBin( } /// - /// + /// Restore an entry from bin /// /// - /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpPut("entries/{entryId:guid}/restore")] public async Task>> RestoreBinEntry( @@ -63,11 +63,12 @@ public async Task>> RestoreBinEntry( return Ok(Result.Succeed(result)); } + /// - /// + /// Remove an entry from bin /// - /// - /// + /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpDelete("entries/{entryId:guid}")] public async Task>> DeleteBinEntry( @@ -85,11 +86,12 @@ public async Task>> DeleteBinEntry( return Ok(Result.Succeed(result)); } + /// - /// + /// Get entry in the bin by Id /// - /// - /// + /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId:guid}")] public async Task>> GetBinEntryById( @@ -108,13 +110,13 @@ public async Task>> GetBinEntryById( } /// - /// + /// Get all entry in the bin /// - /// - /// + /// + /// a paginated list of EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries")] - public async Task>> GetAllBinEntriesPaginated( + public async Task>>> GetAllBinEntriesPaginated( [FromQuery] GetAllBinEntriesPaginatedQueryParameters queryParameters) { var currentUser = _currentUserService.GetCurrentUser(); @@ -132,4 +134,4 @@ public async Task>> GetAllBinEntriesPaginated( var result = await Mediator.Send(command); return Ok(Result>.Succeed(result)); } -} \ No newline at end of file +} diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index b088f822..53c1a4e0 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -23,10 +23,10 @@ public EntriesController(ICurrentUserService currentUserService) } /// - /// + /// Upload a file or create a directory /// /// - /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpPost] public async Task>> UploadEntry( @@ -87,6 +87,12 @@ public async Task>> UploadEntry( return Ok(Result.Succeed(result)); } + /// + /// Update an entry + /// + /// + /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpPut("{entryId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -112,9 +118,9 @@ public async Task>> Update( /// - /// + /// Get all entries paginated /// - /// + /// a paginated list of EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] @@ -133,8 +139,13 @@ public async Task>>> GetAllPaginated var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } + + + /// + /// Get an entry by Id + /// /// - /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("{entryId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -151,10 +162,10 @@ public async Task>> GetById([FromRoute] Guid entry } /// - /// + /// Share an entry /// /// - /// + /// an EntryPermissionDto [RequiresRole(IdentityData.Roles.Employee)] [HttpPut("{entryId:guid}/permissions")] public async Task>> ManagePermission( diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index a7e0945a..928787dd 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -24,9 +24,30 @@ public SharedController(ICurrentUserService currentUserService) } /// - /// + /// Download a shared file /// - /// + /// + /// the download file + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("entries/{entryId:guid}/file")] + public async Task DownloadSharedFile([FromRoute] Guid entryId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new DownloadSharedEntry.Query() + { + CurrentUser = currentUser, + EntryId = entryId, + }; + var result = await Mediator.Send(query); + var content = new MemoryStream(result.FileData); + HttpContext.Response.ContentType = result.FileType; + return File(content, result.FileType, result.FileName); + } + + /// + /// Get all shared entries paginated + /// + /// a paginated list of EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries")] public async Task>> GetAll( @@ -48,7 +69,7 @@ public async Task>> GetAll( /// - /// upload a file or create a directory to a shared entry + /// Upload a file or create a directory to a shared entry /// /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] @@ -116,9 +137,9 @@ public async Task>> CreateSharedEntry([ } /// - /// + /// Get shared users from a shared entries paginated /// - /// + /// a paginated list of UserDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId:guid}/shared-users")] public async Task>> GetSharedUsersFromASharedEntryPaginated( @@ -138,10 +159,13 @@ public async Task>> GetSharedUser return Ok(Result>.Succeed(result)); } + /// - /// + /// Share an entry to a user /// - /// [RequiresRole(IdentityData.Roles.Employee)] + /// + /// an EntryPermissionDto + [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId}/permissions")] public async Task>> SharePermissions( [FromRoute] Guid entryId) @@ -157,12 +181,12 @@ public async Task>> SharePermissions( } /// - /// + /// Get shared entry by Id /// - /// + /// an EntryDto [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("entries/{entryId:guid}")] - public async Task>> GetSharedEntryById( + public async Task>> GetSharedEntryById( [FromRoute] Guid entryId) { var currentUser = _currentUserService.GetCurrentUser(); From 5cbe1a1aac012d4fe578d73dd931903a7149a704 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 01:45:16 +0700 Subject: [PATCH 133/162] abc --- src/Api/Controllers/SharedController.cs | 4 +-- .../Entries/Commands/CreateSharedEntry.cs | 36 +++++-------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 928787dd..29bc6959 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -78,8 +78,8 @@ public async Task>> GetAll( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> CreateSharedEntry([ - FromRoute] Guid entryId, + public async Task>> CreateSharedEntry( + [FromRoute] Guid entryId, [FromForm] UploadSharedEntryRequest request) { var currentUser = _currentUserService.GetCurrentUser(); diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index 59188e5e..41501fc2 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -53,40 +53,25 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimePr public async Task Handle(Command request, CancellationToken cancellationToken) { - var permissions = _context.EntryPermissions + var permission = await _context.EntryPermissions .Include(x => x.Entry) .ThenInclude(y => y.Uploader) .Include(x => x.Entry) .ThenInclude(x => x.Owner) - .Where(x => x.EmployeeId == request.CurrentUser.Id - && x.AllowedOperations.Contains(EntryOperation.Edit.ToString())); - - var rootEntry = await permissions - .Select(x => x.Entry) - .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); - - if (rootEntry is null) + .FirstOrDefaultAsync(x => x.EmployeeId == request.CurrentUser.Id && + x.EntryId == request.EntryId, cancellationToken); + if (permission is null) { - throw new KeyNotFoundException("Entry does not exist."); + throw new UnauthorizedAccessException("User is not allowed to create an entry."); } - if (!rootEntry.IsDirectory) + if (!permission.Entry.IsDirectory) { throw new ConflictException("This is a file."); } - var permission = _context.EntryPermissions.FirstOrDefaultAsync( - x => x.EntryId == rootEntry.Id && - x.EmployeeId == request.CurrentUser.Id && - x.AllowedOperations.Contains(EntryOperation.Edit.ToString()), cancellationToken); - - if (permission is null) - { - throw new UnauthorizedAccessException("User is not allowed to create an entry."); - } - var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - var entryPath = rootEntry.Path + "/" + request.Name.Trim(); + var entryPath = permission.Entry.Path.Equals("/") ? permission.Entry.Path + permission.Entry.Name : $"{permission.Entry.Path}/{request.Name.Trim()}"; var entity = new Entry() { @@ -95,8 +80,8 @@ public async Task Handle(Command request, CancellationToken cancellati CreatedBy = request.CurrentUser.Id, Uploader = request.CurrentUser, Created = localDateTimeNow, - Owner = rootEntry.Owner, - OwnerId = rootEntry.Owner.Id, + Owner = permission.Entry.Owner, + OwnerId = permission.Entry.Owner.Id, }; if (request.IsDirectory) @@ -119,9 +104,6 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("File size must be lower than 20MB"); } - var lastDotIndex = request.Name.LastIndexOf(".", StringComparison.Ordinal); - var fileExtension = request.Name.Substring(lastDotIndex + 1, request.Name.Length - lastDotIndex - 1); - var fileEntity = new FileEntity() { FileData = request.FileData.ToArray(), From 5794e936b6cf8def7db18f8294a5884f022bd00e Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 01:54:32 +0700 Subject: [PATCH 134/162] xxx --- src/Api/Controllers/EntriesController.cs | 2 ++ src/Application/Entries/Queries/GetAllEntriesPaginated.cs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs index 53c1a4e0..064e4c89 100644 --- a/src/Api/Controllers/EntriesController.cs +++ b/src/Api/Controllers/EntriesController.cs @@ -127,8 +127,10 @@ public async Task>> Update( public async Task>>> GetAllPaginated( [FromQuery] GetAllEntriesPaginatedQueryParameters queryParameters ) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetAllEntriesPaginated.Query() { + CurrentUser = currentUser, Page = queryParameters.Page, Size = queryParameters.Size, EntryPath = queryParameters.EntryPath, diff --git a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs index 8a8a2002..53dbdfb0 100644 --- a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Digital; using AutoMapper; +using Domain.Entities; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -25,6 +26,7 @@ public Validator() public record Query : IRequest> { + public User CurrentUser { get; init; } = null!; public int? Page { get; init; } public int? Size { get; init; } public string? SortBy { get; init; } @@ -46,8 +48,8 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var entries = _context.Entries - .Where(x => x.Path.Equals(request.EntryPath)) - .AsQueryable(); + .Where(x => x.Path.Equals(request.EntryPath) && + x.OwnerId == request.CurrentUser.Id); var sortBy = request.SortBy; if (sortBy is null || !sortBy.MatchesPropertyName()) From 866750b2fcb1b3951f249bef4c297c6c32e2e5cf Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 03:34:26 +0700 Subject: [PATCH 135/162] fix --- src/Application/Common/Interfaces/IMailService.cs | 2 +- .../Entries/Commands/CreateSharedEntry.cs | 14 ++++++++++++++ .../Entries/Commands/DownloadDigitalFile.cs | 3 +-- .../Queries/GetAllSharedEntriesPaginated.cs | 2 +- .../Users/EventHandlers/UserCreatedEventHandler.cs | 2 +- src/Infrastructure/Services/MailService.cs | 3 +-- .../CustomMailService.cs | 2 +- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs index 0dbc58c7..1da7a46d 100644 --- a/src/Application/Common/Interfaces/IMailService.cs +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -4,5 +4,5 @@ namespace Application.Common.Interfaces; public interface IMailService { - bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash); + bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword); } \ No newline at end of file diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index 41501fc2..ade8d987 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -6,6 +6,7 @@ using AutoMapper; using Domain.Entities; using Domain.Entities.Digital; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -117,7 +118,20 @@ public async Task Handle(Command request, CancellationToken cancellati entity.File = fileEntity; } + var result = await _context.Entries.AddAsync(entity, cancellationToken); + + var permissionEntity = new EntryPermission() + { + EntryId = result.Entity.Id, + Entry = result.Entity, + EmployeeId = request.CurrentUser.Id, + Employee = request.CurrentUser, + ExpiryDateTime = null, + AllowedOperations = $"{EntryOperation.View.ToString()},{EntryOperation.Edit.ToString()}" + }; + + await _context.EntryPermissions.AddAsync(permissionEntity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entity.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 8ea8fad1..ef020e9a 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -69,7 +69,6 @@ public async Task Handle(Command request, CancellationToken cancellation var content = new MemoryStream(entry.File!.FileData); var fileType = entry.File!.FileType; - var fileExtension = entry.File!.FileExtension; var fileId = entry.File!.Id; using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUser.Id)) @@ -81,7 +80,7 @@ public async Task Handle(Command request, CancellationToken cancellation { Content = content, FileType = fileType, - FileName = $"{entry.Name}.{fileExtension}", + FileName = entry.Name, }; } } diff --git a/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs index 4bc016b8..1f0f9fd0 100644 --- a/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs @@ -67,7 +67,7 @@ public async Task> Handle(Query request, CancellationTok var basePath = baseEntry.Path.Equals("/") ? $"{baseEntry.Path}{baseEntry.Name}" : $"{baseEntry.Path}/{baseEntry.Name}"; - var pattern = $"{baseEntry.Path}/"; + var pattern = $"{basePath}/"; entries = permissions .Select(x => x.Entry) diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs index 41b48e34..17538775 100644 --- a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -35,6 +35,6 @@ public async Task Handle(UserCreatedEvent notification, CancellationToken cancel await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken, cancellationToken); await _authDbContext.SaveChangesAsync(cancellationToken); - _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, token); + _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password); } } \ No newline at end of file diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 8515cad5..62faf977 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -17,7 +17,7 @@ public MailService(IOptions mailSettingsOptions) _mailSettings = mailSettingsOptions.Value; } - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword) { var data = new HtmlMailData() { @@ -34,7 +34,6 @@ public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword TemplateVariables = new TemplateVariables() { UserEmail = userEmail, - ResetPasswordTokenHash = resetPasswordTokenHash, UserPassword = temporaryPassword, }, }; diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs index 32c8d360..aaeabc16 100644 --- a/tests/Application.Tests.Integration/CustomMailService.cs +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -4,7 +4,7 @@ namespace Application.Tests.Integration; public class CustomMailService : IMailService { - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword) { return true; } From 88b62cc0e862a6c97e3c3856ceb6199d4a53ad35 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 13:56:40 +0700 Subject: [PATCH 136/162] sth --- src/Api/Controllers/BinController.cs | 1 + ...etAllBinEntriesPaginatedQueryParameters.cs | 1 + .../Queries/GetAllBinEntriesPaginated.cs | 38 +++++++++++++------ .../Entries/Queries/GetBinEntryById.cs | 2 + 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs index 3b7cd3e2..6b987051 100644 --- a/src/Api/Controllers/BinController.cs +++ b/src/Api/Controllers/BinController.cs @@ -124,6 +124,7 @@ public async Task>>> GetAllBinEntrie var command = new GetAllBinEntriesPaginated.Query() { CurrentUser = currentUser, + EntryPath = queryParameters.EntryPath, Page = queryParameters.Page, Size = queryParameters.Size, SearchTerm = queryParameters.SearchTerm, diff --git a/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs index 9586b72a..19f038fd 100644 --- a/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs @@ -2,5 +2,6 @@ public class GetAllBinEntriesPaginatedQueryParameters : PaginatedQueryParameters { + public string EntryPath { get; set; } = null!; public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs index 2c21f199..7f88e936 100644 --- a/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs @@ -15,6 +15,7 @@ public class GetAllBinEntriesPaginated public record Query : IRequest> { public User CurrentUser { get; init; } = null!; + public string EntryPath { get; init; } = null!; public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -35,13 +36,15 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var pattern = $"{request.CurrentUser.Username}_bin/%"; + var realPath = $"{request.CurrentUser.Username}_bin{request.EntryPath}"; + var count1 = $"{request.CurrentUser.Username}_bin".Length; var entries = _context.Entries .Include(x => x.Owner) + .Include(x => x.File) .AsQueryable() - .Where(x => x.Owner.Username.Equals(request.CurrentUser.Username) - && EF.Functions.Like(x.Path, pattern)); + .Where(x => x.Owner.Id == request.CurrentUser.Id + && x.Path == realPath); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { @@ -49,14 +52,27 @@ public async Task> Handle(Query request, CancellationTok x.Name.Contains(request.SearchTerm.Trim())); } - return await entries - .ListPaginateWithSortAsync( - request.Page, - request.Size, - request.SortBy, - request.SortOrder, - _mapper.ConfigurationProvider, - cancellationToken); + var sortBy = request.SortBy; + + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(EntryDto.Id); + } + + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 10 : request.Size; + + var count = await entries.CountAsync(cancellationToken); + var list = await entries + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + list.ForEach(x => x.Path = x.Path[count1..]); + + var result = _mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } } } \ No newline at end of file diff --git a/src/Application/Entries/Queries/GetBinEntryById.cs b/src/Application/Entries/Queries/GetBinEntryById.cs index df2e831d..44213b79 100644 --- a/src/Application/Entries/Queries/GetBinEntryById.cs +++ b/src/Application/Entries/Queries/GetBinEntryById.cs @@ -53,6 +53,8 @@ public async Task Handle(Query request, CancellationToken cancellation throw new UnauthorizedAccessException("You do not have permission to view this entry"); } + entry.Path = entry.Path[binCheck.Length..]; + return _mapper.Map(entry); } } From b60f7cb887a1005acf5261fed587b56bd9dbcecd Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:56:43 +0700 Subject: [PATCH 137/162] refactor logic (#379) --- src/Application/Entries/Commands/RestoreBinEntry.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index 03f0ea54..da77e0f8 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -86,6 +86,11 @@ public async Task Handle(Command request, CancellationToken cancellati { continue; } + + if (currentPath.Equals("/") && node.Equals("")) + { + continue; + } var entrySearch = await _context.Entries.FirstOrDefaultAsync(x => x.Path.Equals(currentPath) && x.Name.Equals(node), cancellationToken); From d60b1f0308ddaad0d603b9bbb851f3a6f2598f12 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 12 Jul 2023 14:52:22 +0700 Subject: [PATCH 138/162] fix: admin can see borrow request by id (#380) * fix: admin can see borrow request by id * la la la la --- src/Api/Controllers/BorrowsController.cs | 2 +- .../Borrows/Queries/GetBorrowRequestById.cs | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 0503bb34..7f49b61e 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -51,7 +51,7 @@ public async Task>> BorrowDocument( /// Get a borrow request by id /// /// A BorrowDto of the retrieved borrow - [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [RequiresRole(IdentityData.Roles.Admin ,IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{borrowId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/src/Application/Borrows/Queries/GetBorrowRequestById.cs b/src/Application/Borrows/Queries/GetBorrowRequestById.cs index 64486fc5..d747f7c4 100644 --- a/src/Application/Borrows/Queries/GetBorrowRequestById.cs +++ b/src/Application/Borrows/Queries/GetBorrowRequestById.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; @@ -34,6 +35,10 @@ public async Task Handle(Query request, CancellationToken cancellatio .Include(x => x.Borrower) .Include(x => x.Document) .ThenInclude(y => y.Department) + .Include(x => x.Document) + .ThenInclude(x => x.Folder) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); if (borrow is null) @@ -41,13 +46,36 @@ public async Task Handle(Query request, CancellationToken cancellatio throw new KeyNotFoundException("Borrow request does not exist."); } - if (!request.User.Role.Equals(IdentityData.Roles.Employee)) + if (request.User.Role.IsAdmin()) + { + return _mapper.Map(borrow); + } + + if (request.User.Role.IsStaff()) { + var staff = _context.Staffs + .Include(x => x.Room) + .FirstOrDefault(x => x.Id == request.User.Id); + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not manage a room."); + } + + if (staff.Room.Id != borrow.Document.Folder!.Locker.Room.Id ) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + return _mapper.Map(borrow); } return borrow.Borrower.Id != request.User.Id - ? throw new UnauthorizedAccessException() + ? throw new UnauthorizedAccessException("User can not access this resource") : _mapper.Map(borrow); } } From b28e215ca71980553c702caec34a3dfbf66c5370 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 15:18:23 +0700 Subject: [PATCH 139/162] fix --- src/Application/Entries/Commands/MoveEntryToBin.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index d49242b6..18cdb470 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -72,6 +72,10 @@ public async Task Handle(Command request, CancellationToken cancellati x.Path.Trim().Equals(path) || EF.Functions.Like(x.Path, pattern)); + var ids = childEntries.Select(x => x.Id); + var permissions = _context.EntryPermissions.Where(x => ids.Contains(x.EntryId)); + _context.EntryPermissions.RemoveRange(permissions); + foreach (var childEntry in childEntries) { var childBinPath = ownerUsername + BinString + childEntry.Path; @@ -86,7 +90,10 @@ public async Task Handle(Command request, CancellationToken cancellati entry.Path = binPath; entry.LastModified = localDateTimeNow; entry.LastModifiedBy = request.CurrentUser.Id; - + + var entryPermissions = _context.EntryPermissions.Where(x => x.EntryId == entry.Id); + _context.EntryPermissions.RemoveRange(entryPermissions); + var result = _context.Entries.Update(entry); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(entry), entry.Id, request.CurrentUser.Id)) From 766dafd3a71301de26539d9b39c8304f6a590fb1 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:22:46 +0700 Subject: [PATCH 140/162] fix: get all users include department (#381) --- src/Application/Users/Queries/GetAllUsersPaginated.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index 27d57746..9fb451ad 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -5,6 +5,7 @@ using AutoMapper; using Domain.Entities; using MediatR; +using Microsoft.EntityFrameworkCore; namespace Application.Users.Queries; @@ -35,6 +36,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var users = _context.Users + .Include(x => x.Department) .Where(x => !x.Role.Equals(IdentityData.Roles.Admin)); // Filter by department From f2bf83b4370d25e01d385cb2e94de6c300a84f90 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 17:30:12 +0700 Subject: [PATCH 141/162] haha --- .../Entries/Commands/DeleteBinEntry.cs | 5 +++-- .../Entries/Commands/MoveEntryToBin.cs | 11 +++++++++-- .../Entries/Commands/RestoreBinEntry.cs | 14 +++++++++----- .../Queries/GetAllBinEntriesPaginated.cs | 18 +++++++++++++----- src/Domain/Entities/Digital/Entry.cs | 2 ++ .../ApplicationDbContextModelSnapshot.cs | 3 +++ 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Application/Entries/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs index 0b0ada6b..10379f82 100644 --- a/src/Application/Entries/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -48,9 +48,10 @@ public async Task Handle(Command request, CancellationToken cancellati var entryPath = entry.Path; var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); - var binCheck = entryPath.Substring(0, firstSlashIndex); + var binCheck = firstSlashIndex < 0 ? entryPath : entryPath.Substring(0, firstSlashIndex); - if (!binCheck.Contains(BinString)) + var path1 = request.CurrentUser.Username + BinString; + if (!binCheck.Equals(path1)) { throw new NotChangedException("Entry is not in bin."); } diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index 18cdb470..e83c245b 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -76,9 +76,15 @@ public async Task Handle(Command request, CancellationToken cancellati var permissions = _context.EntryPermissions.Where(x => ids.Contains(x.EntryId)); _context.EntryPermissions.RemoveRange(permissions); + var c = entry.Path.Length; foreach (var childEntry in childEntries) { - var childBinPath = ownerUsername + BinString + childEntry.Path; + var childBinPath = ownerUsername + BinString; + if (!entry.Path.Equals("/")) + { + childBinPath += childEntry.Path[c..]; + } // /x /x abc /x/abc de => _bin + /x abc _bin/x/abc de + childEntry.OldPath = childEntry.Path; childEntry.Path = childBinPath; childEntry.LastModified = localDateTimeNow; childEntry.LastModifiedBy = request.CurrentUser.Id; @@ -86,7 +92,8 @@ public async Task Handle(Command request, CancellationToken cancellati } } - var binPath = ownerUsername + BinString + entryPath; + var binPath = $"{ownerUsername}{BinString}"; + entry.OldPath = entry.Path; entry.Path = binPath; entry.LastModified = localDateTimeNow; entry.LastModifiedBy = request.CurrentUser.Id; diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index da77e0f8..da924ade 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -48,14 +48,15 @@ public async Task Handle(Command request, CancellationToken cancellati var entryPath = entry.Path; var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); - var binCheck = entryPath.Substring(0, firstSlashIndex); + var binCheck = firstSlashIndex < 0 ? entryPath : entryPath.Substring(0, firstSlashIndex); - if (!binCheck.Contains(BinString)) + var path1 = request.CurrentUser.Username + BinString; + if (!binCheck.Equals(path1)) { throw new NotChangedException("Entry is not in bin."); } - if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) + if (entry.Owner.Id != request.CurrentUser.Id) { throw new UnauthorizedAccessException("You do not have the permission to restore this entry."); } @@ -78,7 +79,7 @@ public async Task Handle(Command request, CancellationToken cancellati return _mapper.Map(dupeResult.Entity); } - var splitPath = entry.Path.Split("/"); + var splitPath = entry.OldPath!.Split("/"); var currentPath = "/"; foreach (var node in splitPath) { @@ -97,6 +98,7 @@ public async Task Handle(Command request, CancellationToken cancellati if (entrySearch is not null) { + currentPath = currentPath.Equals("/") ? currentPath + node : currentPath + "/" + node; continue; } @@ -125,13 +127,15 @@ public async Task Handle(Command request, CancellationToken cancellati foreach (var childEntry in childEntries) { childEntry.Path = childEntry.Path.Replace(binCheck, ""); + childEntry.OldPath = null; childEntry.LastModified = localDateTimeNow; childEntry.LastModifiedBy = request.CurrentUser.Id; _context.Entries.Update(childEntry); } } - entry.Path = entryPath.Replace(binCheck, ""); + entry.Path = entry.OldPath; + entry.OldPath = null; entry.LastModified = localDateTimeNow; entry.LastModifiedBy = request.CurrentUser.Id; diff --git a/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs index 7f88e936..8e5742b9 100644 --- a/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllBinEntriesPaginated.cs @@ -36,15 +36,23 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var realPath = $"{request.CurrentUser.Username}_bin{request.EntryPath}"; - var count1 = $"{request.CurrentUser.Username}_bin".Length; + var path = $"{request.CurrentUser.Username}_bin"; + var count1 = path.Length; + + if (!request.EntryPath.Equals("/")) + { + path += request.EntryPath; + } var entries = _context.Entries .Include(x => x.Owner) .Include(x => x.File) - .AsQueryable() .Where(x => x.Owner.Id == request.CurrentUser.Id - && x.Path == realPath); + && x.Path == path); + + + // var realPath = $"{request.CurrentUser.Username}_bin{request.EntryPath}"; + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { @@ -69,7 +77,7 @@ public async Task> Handle(Query request, CancellationTok .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); - list.ForEach(x => x.Path = x.Path[count1..]); + list.ForEach(x => { x.Path = x.Path.Length == count1 ? "/" : x.Path[count1..]; }); var result = _mapper.Map>(list); return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs index 0cf344d1..2d7212d1 100644 --- a/src/Domain/Entities/Digital/Entry.cs +++ b/src/Domain/Entities/Digital/Entry.cs @@ -7,10 +7,12 @@ public class Entry : BaseAuditableEntity { public string Name { get; set; } = null!; public string Path { get; set; } = null!; + public string? OldPath { get; set; } public Guid? FileId { get; set; } public Guid OwnerId { get; set; } public long? SizeInBytes { get; set; } [NotMapped] public bool IsDirectory => FileId is null; + [NotMapped] public bool IsInBin => OldPath is not null; public virtual FileEntity? File { get; set; } public virtual User Uploader { get; set; } = null!; diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index eaef20a9..68850ac9 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -105,6 +105,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("OldPath") + .HasColumnType("text"); + b.Property("OwnerId") .HasColumnType("uuid"); From ec4388f049c02bd90ed029fdf66995b7d2210a8e Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 12 Jul 2023 21:34:38 +0700 Subject: [PATCH 142/162] I forgot to ship this --- .../20230712085900_EntryOldPath.Designer.cs | 889 ++++++++++++++++++ .../Migrations/20230712085900_EntryOldPath.cs | 28 + 2 files changed, 917 insertions(+) create mode 100644 src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.Designer.cs new file mode 100644 index 00000000..7896fe38 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.Designer.cs @@ -0,0 +1,889 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230712085900_EntryOldPath")] + partial class EntryOldPath + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Application.Common.Models.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Event") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("ObjectType") + .HasColumnType("text"); + + b.Property("Template") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Logs"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OldPath") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeInBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsSharedRoot") + .HasColumnType("boolean"); + + b.HasKey("EntryId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EntryPermissions"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.User", "Uploader") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.HasOne("Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Owner"); + + b.Navigation("Uploader"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.EntryPermission", b => + { + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "FileId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("File"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.cs b/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.cs new file mode 100644 index 00000000..216592b4 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230712085900_EntryOldPath.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class EntryOldPath : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OldPath", + table: "Entries", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OldPath", + table: "Entries"); + } + } +} From 90e5a084bcf89a377b1b7809bb8f330976b310a1 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 13 Jul 2023 00:24:56 +0700 Subject: [PATCH 143/162] seed --- src/Infrastructure/Infrastructure.csproj | 1 + .../Persistence/ApplicationDbContextSeed.cs | 555 +++++++++++++++--- 2 files changed, 488 insertions(+), 68 deletions(-) diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index e5c36a93..ba75dbc4 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index 6725f57e..5bb6d6a5 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -1,21 +1,25 @@ using Application.Helpers; using Application.Identity; +using Bogus; using Domain.Entities; using Domain.Entities.Physical; +using Domain.Statuses; using Infrastructure.Shared; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using NodaTime; +using Org.BouncyCastle.Crypto.Macs; using Serilog; using Serilog.Context; namespace Infrastructure.Persistence; -public class ApplicationDbContextSeed +public static class ApplicationDbContextSeed { public static async Task Seed(ApplicationDbContext context, IConfiguration configuration, ILogger logger) { if (!configuration.GetValue("Seed")) return; - + var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); try { @@ -30,18 +34,27 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi private static async Task TrySeedAsync(ApplicationDbContext context, string pepper, ILogger logger) { - var department = new Department() + await context.SeedAdminDepartment(pepper); + await context.SeedItDepartment(pepper); + await context.SeedHrDepartment(pepper); + await context.SeedAccountingDepartment(pepper); + + await context.SaveChangesAsync(); + } + + private static async Task SeedAdminDepartment(this ApplicationDbContext context, string pepper) + { + var salt = StringUtil.RandomSalt(); + + var adminDepartment = new Department() { Name = "Admin" }; - var salt = StringUtil.RandomSalt(); - - // Default users var admin = new User { - Username = "admin", - Email = "admin@profile.dev", + Username = "admin", + Email = "admin@profile.dev", PasswordHash = "admin".HashPasswordWith(salt, pepper), PasswordSalt = salt, IsActive = true, @@ -49,99 +62,505 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp Created = LocalDateTime.FromDateTime(DateTime.UtcNow), Role = IdentityData.Roles.Admin, }; + + await context.TryAddDepartmentWith(adminDepartment, + new List() + { + admin + }, + Enumerable.Empty()); + } + + private static async Task SeedItDepartment(this ApplicationDbContext context, string pepper) + { + var itDepartment = new Department() + { + Name = "IT" + }; + + var employee1 = CreateEmployee("employee", "employee@profile.dev", "employee", pepper); + var employee2 = CreateRandomEmployee("johndoe", pepper); + var employee3 = CreateRandomEmployee("johnnysins", pepper); + + var staffUser = CreateStaff("staff", "staff@profile.dev", "staff", pepper); + var staffUser2 = CreateStaff("staff2", "staff2@profile.dev", "staff", pepper); + + await context.TryAddDepartmentWith(itDepartment, + new List() + { + employee1, + employee2, + employee3, + }, + new List() + { + staffUser, + staffUser2, + }); + + var d1 = CreateNDocument(employee1, itDepartment, 5); + d1.AddRange(CreateNDocument(employee2, itDepartment, 4)); + var folder1 = CreateFolder("Short-term contracts 1", "Short term contracts", 20,d1); + var d2 = CreateNDocument(employee3, itDepartment, 8); + d2.AddRange(CreateNDocument(employee2, itDepartment, 7)); + var folder2 = CreateFolder("Short-term contracts 2", "Short-term contracts 2", 15, + CreateNDocument(employee2, itDepartment, 7)); + var d3 = CreateNDocument(employee1, itDepartment, 3); + d3.AddRange(CreateNDocument(employee2, itDepartment, 7)); + d3.AddRange(CreateNDocument(employee3, itDepartment, 4)); + var folder3 = CreateFolder("Long-term contracts 1", "Long-term contracts 1", 30, d3); + var d4 = CreateNDocument(employee1, itDepartment, 2); + d4.AddRange(CreateNDocument(employee2, itDepartment, 9)); + d4.AddRange(CreateNDocument(employee3, itDepartment, 4)); + var folder4 = CreateFolder("Long-term contracts 2", "Long-term contracts 2", 20, d4); + var d5 = CreateNDocument(employee1, itDepartment, 5); + d5.AddRange(CreateNDocument(employee2, itDepartment, 1)); + d5.AddRange(CreateNDocument(employee3, itDepartment, 8)); + var folder5 = CreateFolder("Long-term contracts 3", "Long-term contracts 3", 20, d5); + var d6 = CreateNDocument(employee1, itDepartment, 5); + d6.AddRange(CreateNDocument(employee2, itDepartment, 5)); + d6.AddRange(CreateNDocument(employee3, itDepartment, 5)); + var folder6 = CreateFolder("SRS", "Software requirement specification", 20, d6); + var d7 = CreateNDocument(employee1, itDepartment, 1); + d7.AddRange(CreateNDocument(employee2, itDepartment, 1)); + d7.AddRange(CreateNDocument(employee3, itDepartment, 12)); + var folder7 = CreateFolder("SDS", "Software design specification", 20, d7); + var d8 = CreateNDocument(employee1, itDepartment, 7); + d8.AddRange(CreateNDocument(employee2, itDepartment, 5)); + d8.AddRange(CreateNDocument(employee3, itDepartment, 1)); + var folder8 = CreateFolder("Private invoices", "Private invoices", 20, d8); + var d9 = CreateNDocument(employee1, itDepartment, 5); + d9.AddRange(CreateNDocument(employee2, itDepartment, 8)); + d9.AddRange(CreateNDocument(employee3, itDepartment, 1)); + var folder9 = CreateFolder("Public invoices", "Public invoices", 20, d9); + + var locker1 = CreateLocker("Contracts", "Contracts", 20, new List() + { + folder1, + folder2, + folder3, + folder4, + folder5, + }); + var locker2 = CreateLocker("Specifications", "All types of specifications", 20, new List() + { + folder6, + folder7, + }); + var locker3 = CreateLocker("Invoices", "Invoices related to products", 20, new List() + { + folder8, + folder9, + }); + + var locker4 = CreateLocker("Papers", "All research papers related", 20, new List() + { + + }); + var locker5 = CreateLocker("Reports", "Annually, Monthly, Weekly reports", 20, new List()); + await context.TryAddRoom("IT Storage", "Storage for IT Department", 40, itDepartment, staffUser, new List() + { + locker1, + locker2, + locker3, + }); + await context.TryAddRoom("IT Storage 2", "Storage 2 for IT Department", 30, itDepartment, staffUser2, new List() + { + locker4, + locker5, + }); + } + + private static async Task SeedHrDepartment(this ApplicationDbContext context, string pepper) + { + var hrDepartment = new Department + { + Name = "Human Resource" + }; + + var employee1 = CreateRandomEmployee("cuonglt2", pepper); + var employee2 = CreateRandomEmployee("thanht", pepper); + var employee3 = CreateRandomEmployee("chaum", pepper); + var employee4 = CreateRandomEmployee("tuantt", pepper); + + var staff = CreateStaff("maint", "maint@profile.dev", "employee", pepper); + + await context.TryAddDepartmentWith(hrDepartment, + new List() + { + employee1, + employee2, + employee3, + employee4, + }, + new List() + { + staff, + }); - salt = StringUtil.RandomSalt(); - var staffUser = new User() + var d1 = CreateNDocument(employee1, hrDepartment, 5); + d1.AddRange(CreateNDocument(employee2, hrDepartment, 4)); + var folder1 = CreateFolder("Short-term contracts 1", "Short term contracts", 20,d1); + var d2 = CreateNDocument(employee3, hrDepartment, 8); + d2.AddRange(CreateNDocument(employee2, hrDepartment, 7)); + var folder2 = CreateFolder("Short-term contracts 2", "Short-term contracts 2", 15, + CreateNDocument(employee2, hrDepartment, 7)); + var d3 = CreateNDocument(employee1, hrDepartment, 3); + d3.AddRange(CreateNDocument(employee2, hrDepartment, 7)); + d3.AddRange(CreateNDocument(employee3, hrDepartment, 4)); + var folder3 = CreateFolder("Long-term contracts 1", "Long-term contracts 1", 30, d3); + var d4 = CreateNDocument(employee1, hrDepartment, 2); + d4.AddRange(CreateNDocument(employee2, hrDepartment, 9)); + d4.AddRange(CreateNDocument(employee3, hrDepartment, 4)); + var folder4 = CreateFolder("Long-term contracts 2", "Long-term contracts 2", 20, d4); + var d5 = CreateNDocument(employee1, hrDepartment, 5); + d5.AddRange(CreateNDocument(employee2, hrDepartment, 1)); + d5.AddRange(CreateNDocument(employee3, hrDepartment, 8)); + var folder5 = CreateFolder("Long-term contracts 3", "Long-term contracts 3", 20, d5); + var d6 = CreateNDocument(employee1, hrDepartment, 5); + d6.AddRange(CreateNDocument(employee2, hrDepartment, 5)); + d6.AddRange(CreateNDocument(employee3, hrDepartment, 5)); + var folder6 = CreateFolder("SRS", "Software requirement specification", 20, d6); + var d7 = CreateNDocument(employee1, hrDepartment, 1); + d7.AddRange(CreateNDocument(employee2, hrDepartment, 1)); + d7.AddRange(CreateNDocument(employee3, hrDepartment, 12)); + var folder7 = CreateFolder("SDS", "Software design specification", 20, d7); + var d8 = CreateNDocument(employee1, hrDepartment, 7); + d8.AddRange(CreateNDocument(employee2, hrDepartment, 5)); + d8.AddRange(CreateNDocument(employee3, hrDepartment, 1)); + var folder8 = CreateFolder("Private invoices", "Private invoices", 20, d8); + var d9 = CreateNDocument(employee1, hrDepartment, 5); + d9.AddRange(CreateNDocument(employee2, hrDepartment, 8)); + d9.AddRange(CreateNDocument(employee3, hrDepartment, 1)); + var folder9 = CreateFolder("Public invoices", "Public invoices", 20, d9); + + var locker1 = CreateLocker("Contracts", "Contracts", 20, new List() + { + folder1, + folder2, + folder3, + folder4, + folder5, + }); + var locker2 = CreateLocker("Specifications", "All types of specifications", 20, new List() + { + folder6, + folder7, + }); + var locker3 = CreateLocker("Invoices", "Invoices related to products", 20, new List() { - Username = "staff", - Email = "staff@profile.dev", - PasswordHash = "staff".HashPasswordWith(salt, pepper), + folder8, + folder9, + }); + + var locker4 = CreateLocker("Papers", "All research papers related", 20, new List() + { + + }); + var locker5 = CreateLocker("Reports", "Annually, Monthly, Weekly reports", 20, new List()); + await context.TryAddRoom("HR Storage", "Storage for IT Department", 40, hrDepartment, staff, new List() + { + locker1, + locker2, + locker3, + locker4, + locker5, + }); + } + + private static async Task SeedAccountingDepartment(this ApplicationDbContext context, string pepper) + { + var accountingDepartment = new Department + { + Name = "Accounting" + }; + + var employee1 = CreateRandomEmployee("peterparker", pepper); + var employee2 = CreateRandomEmployee("maryjane", pepper); + var employee3 = CreateRandomEmployee("gwenstacy", pepper); + var employee4 = CreateRandomEmployee("miles", pepper); + + var staff = CreateStaff("eddie", "eddie@profile.dev", "employee", pepper); + + await context.TryAddDepartmentWith(accountingDepartment, + new List() + { + employee1, + employee2, + employee3, + employee4, + }, + new List() + { + staff, + }); + + + var d1 = CreateNDocument(employee1, accountingDepartment, 5); + d1.AddRange(CreateNDocument(employee2, accountingDepartment, 4)); + var folder1 = CreateFolder("Short-term contracts 1", "Short term contracts", 20,d1); + var d2 = CreateNDocument(employee3, accountingDepartment, 8); + d2.AddRange(CreateNDocument(employee2, accountingDepartment, 7)); + var folder2 = CreateFolder("Short-term contracts 2", "Short-term contracts 2", 15, + CreateNDocument(employee2, accountingDepartment, 7)); + var d3 = CreateNDocument(employee1, accountingDepartment, 3); + d3.AddRange(CreateNDocument(employee2, accountingDepartment, 7)); + d3.AddRange(CreateNDocument(employee3, accountingDepartment, 4)); + var folder3 = CreateFolder("Long-term contracts 1", "Long-term contracts 1", 30, d3); + var d4 = CreateNDocument(employee1, accountingDepartment, 2); + d4.AddRange(CreateNDocument(employee2, accountingDepartment, 9)); + d4.AddRange(CreateNDocument(employee3, accountingDepartment, 4)); + var folder4 = CreateFolder("Long-term contracts 2", "Long-term contracts 2", 20, d4); + var d5 = CreateNDocument(employee1, accountingDepartment, 5); + d5.AddRange(CreateNDocument(employee2, accountingDepartment, 1)); + d5.AddRange(CreateNDocument(employee3, accountingDepartment, 8)); + var folder5 = CreateFolder("Long-term contracts 3", "Long-term contracts 3", 20, d5); + var d6 = CreateNDocument(employee1, accountingDepartment, 5); + d6.AddRange(CreateNDocument(employee2, accountingDepartment, 5)); + d6.AddRange(CreateNDocument(employee3, accountingDepartment, 5)); + var folder6 = CreateFolder("SRS", "Software requirement specification", 20, d6); + var d7 = CreateNDocument(employee1, accountingDepartment, 1); + d7.AddRange(CreateNDocument(employee2, accountingDepartment, 1)); + d7.AddRange(CreateNDocument(employee3, accountingDepartment, 12)); + var folder7 = CreateFolder("SDS", "Software design specification", 20, d7); + var d8 = CreateNDocument(employee1, accountingDepartment, 7); + d8.AddRange(CreateNDocument(employee2, accountingDepartment, 5)); + d8.AddRange(CreateNDocument(employee3, accountingDepartment, 1)); + var folder8 = CreateFolder("Private invoices", "Private invoices", 20, d8); + var d9 = CreateNDocument(employee1, accountingDepartment, 5); + d9.AddRange(CreateNDocument(employee2, accountingDepartment, 8)); + d9.AddRange(CreateNDocument(employee3, accountingDepartment, 1)); + var folder9 = CreateFolder("Public invoices", "Public invoices", 20, d9); + + var locker1 = CreateLocker("Contracts", "Contracts", 20, new List() + { + folder1, + folder2, + folder3, + folder4, + folder5, + }); + var locker2 = CreateLocker("Specifications", "All types of specifications", 20, new List() + { + folder6, + folder7, + }); + var locker3 = CreateLocker("Invoices", "Invoices related to products", 20, new List() + { + folder8, + folder9, + }); + + var locker4 = CreateLocker("Papers", "All research papers related", 20, new List() + { + + }); + var locker5 = CreateLocker("Reports", "Annually, Monthly, Weekly reports", 20, new List()); + await context.TryAddRoom("Accounting Storage", "Storage for Accounting Department", 40, accountingDepartment, staff, new List() + { + locker1, + locker2, + locker3, + locker4, + locker5, + }); + } + + private static User CreateRandomEmployee(string username, string pepper) + { + var salt = StringUtil.RandomSalt(); + return new User() + { + Username = username, + Email = new Faker().Person.Email, + FirstName = new Faker().Person.FirstName, + LastName = new Faker().Person.LastName, + PasswordHash = new Faker().Random.String().HashPasswordWith(salt, pepper), PasswordSalt = salt, IsActive = true, IsActivated = true, Created = LocalDateTime.FromDateTime(DateTime.UtcNow), - Role = IdentityData.Roles.Staff, - }; - - var itDepartment = new Department() - { - Name = "IT" + Role = IdentityData.Roles.Employee, }; - salt = StringUtil.RandomSalt(); - var employee = new User() + } + + private static User CreateEmployee(string username, string email, string password, string pepper) + { + var salt = StringUtil.RandomSalt(); + return new User() { - Username = "employee", - Email = "employee@profile.dev", - PasswordHash = "employee".HashPasswordWith(salt, pepper), + Username = username, + Email = email, + FirstName = new Faker().Person.FirstName, + LastName = new Faker().Person.LastName, + PasswordHash = password.HashPasswordWith(salt, pepper), PasswordSalt = salt, IsActive = true, IsActivated = true, Created = LocalDateTime.FromDateTime(DateTime.UtcNow), Role = IdentityData.Roles.Employee, }; - - var staff = new Staff() + } + + private static User CreateStaff(string username, string email, string password, string pepper) + { + var salt = StringUtil.RandomSalt(); + return new User() { - User = staffUser, - Room = null, + Username = username, + Email = email, + FirstName = new Faker().Person.FirstName, + LastName = new Faker().Person.LastName, + PasswordHash = password.HashPasswordWith(salt, pepper), + PasswordSalt = salt, + IsActive = true, + IsActivated = true, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Role = IdentityData.Roles.Staff, }; - + } + + private static async Task TryAddDepartmentWith(this ApplicationDbContext context, Department department, + IEnumerable users, IEnumerable staffs) + { if (context.Departments.All(u => u.Name != department.Name)) { await context.Departments.AddAsync(department); - if (context.Users.All(u => u.Username != admin.Username)) - { - admin.Department = department; - await context.Users.AddAsync(admin); - } + await context.AddUsers(department, users); + await context.AddStaffs(department, staffs); } else { var departmentEntity = context.Departments.Single(x => x.Name.Equals(department.Name)); - if (context.Users.All(u => u.Username != admin.Username)) - { - admin.Department = departmentEntity; - await context.Users.AddAsync(admin); - } + await context.AddUsers(departmentEntity, users); + await context.AddStaffs(departmentEntity, staffs); } - - if (context.Departments.All(u => u.Name != itDepartment.Name)) + } + + private static async Task TryAddRoom(this ApplicationDbContext context, string name, + string description, int capacity, Department department, User user, ICollection lockers) + { + var staff = await context.Staffs.FirstOrDefaultAsync(x => x.User.Id == user.Id); + if (context.Rooms.All(x => x.Name != name)) { - await context.Departments.AddAsync(itDepartment); - if (context.Users.All(u => u.Username != employee.Username)) - { - employee.Department = itDepartment; - await context.Users.AddAsync(employee); - } - if (context.Users.All(u => u.Username != staffUser.Username)) - { - staffUser.Department = itDepartment; - await context.Users.AddAsync(staffUser); - } - if (context.Staffs.All(s => s.User.Username != staff.User.Username)) + var room = new Room { - await context.Staffs.AddAsync(staff); - } + Name = name, + Department = department, + DepartmentId = department.Id, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Staff = staff, + Lockers = lockers, + Capacity = capacity, + NumberOfLockers = lockers.Count, + IsAvailable = true, + Description = description, + }; + await context.Rooms.AddAsync(room); } - else + } + + private static Locker CreateLocker(string name, string description, int capacity, ICollection folders) + { + return new Locker + { + Name = name, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Capacity = capacity, + IsAvailable = true, + Description = description, + Folders = folders, + NumberOfFolders = folders.Count, + }; + } + + private static Folder CreateFolder(string name, string description, int capacity, ICollection documents) + { + return new Folder { - var departmentEntity = context.Departments.Single(x => x.Name.Equals(itDepartment.Name)); - if (context.Users.All(u => u.Username != employee.Username)) + Name = name, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Capacity = capacity, + IsAvailable = true, + Description = description, + Documents = documents, + NumberOfDocuments = documents.Count, + }; + } + + private static Document CreateDocument(User user, Department department) + { + return new Document + { + Title = new Faker().Name.JobArea(), + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Description = new Faker().Commerce.ProductDescription(), + DocumentType = new Faker().Commerce.Categories(1)[0], + Department = department, + Importer = user, + ImporterId = user.Id, + IsPrivate = new Faker().Random.Bool(), + Status = Random(), + CreatedBy = user.Id, + }; + } + + private static DocumentStatus Random() + { + var r = new Random().Next(0, 4); + + return r switch + { + 0 => DocumentStatus.Available, + 1 => DocumentStatus.Borrowed, + 2 => DocumentStatus.Issued, + 3 => DocumentStatus.Lost, + _ => DocumentStatus.Available + }; + } + + private static List CreateNDocument(User user, Department department, int n) + { + var list = new List(); + for (var i = 0; i < n; i++) + { + list.Add(CreateDocument(user,department)); + } + + return list; + } + + private static async Task AddUsers(this ApplicationDbContext context, Department department, + IEnumerable users) + { + foreach (var user in users) + { + if (!context.Users.All(u => u.Username != user.Username)) continue; + user.Department = department; + await context.Users.AddAsync(user); + } + } + + private static async Task AddStaffs(this ApplicationDbContext context, Department department, + IEnumerable staffs) + { + foreach (var staff in staffs) + { + if (context.Users.All(u => u.Username != staff.Username)) { - employee.Department = departmentEntity; - await context.Users.AddAsync(employee); + staff.Department = department; + await context.Users.AddAsync(staff); } - if (context.Users.All(u => u.Username != staffUser.Username)) + + var staffEntity = new Staff { - staffUser.Department = itDepartment; - await context.Users.AddAsync(staffUser); - } - if (context.Staffs.All(s => s.User.Username != staff.User.Username)) + User = staff, + Room = null, + }; + if (context.Staffs.All(s => s.User.Username != staff.Username)) { - await context.Staffs.AddAsync(staff); + await context.Staffs.AddAsync(staffEntity); } } - - await context.SaveChangesAsync(); } } \ No newline at end of file From ecf6a5f6e14af9e69a60de6a59cfe04980398034 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 13 Jul 2023 03:44:06 +0700 Subject: [PATCH 144/162] x --- src/Api/Controllers/DocumentsController.cs | 33 ++++++++ .../Documents/Commands/DeleteDocument.cs | 2 - .../Commands/UploadFileToDocument.cs | 81 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/Application/Documents/Commands/UploadFileToDocument.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 7cd13015..fda1be1c 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -304,4 +304,37 @@ public async Task DownloadFile( HttpContext.Response.ContentType = result.FileType; return File(content, result.FileType, result.FileName); } + + /// + /// Upload a file to link with a document + /// + /// Document id + /// + /// File + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("{documentId:guid}/file")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> UploadFile( + [FromRoute] Guid documentId, + IFormFile file) + { + var currentUser = _currentUserService.GetCurrentUser(); + var fileData = new MemoryStream(); + await file.CopyToAsync(fileData); + var lastDotIndex = file.FileName.LastIndexOf(".", StringComparison.Ordinal); + var extension = file.FileName.Substring(lastDotIndex + 1, file.FileName.Length - lastDotIndex - 1); + + var command = new UploadFileToDocument.Command() + { + CurrentUser = currentUser, + DocumentId = documentId, + FileData = fileData, + FileType = file.ContentType, + FileExtension = extension, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } } diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 0294711f..176a6845 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -45,8 +45,6 @@ public async Task Handle(Command request, CancellationToken cancell throw new KeyNotFoundException("Document does not exist."); } - var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - if (document.Folder is not null) { document.Folder.NumberOfDocuments -= 1; diff --git a/src/Application/Documents/Commands/UploadFileToDocument.cs b/src/Application/Documents/Commands/UploadFileToDocument.cs new file mode 100644 index 00000000..989114a1 --- /dev/null +++ b/src/Application/Documents/Commands/UploadFileToDocument.cs @@ -0,0 +1,81 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Digital; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Commands; + +public abstract class UploadFileToDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + public MemoryStream FileData { get; init; } = null!; + public string FileType { get; init; } = null!; + public string FileExtension { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.File) + .Include(x => x.Importer) + .Include(x => x.Department) + .Include(x => x.Folder) + .ThenInclude(y => y!.Locker) + .ThenInclude(z => z.Room) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (request.CurrentUser.Role.IsEmployee() && document.ImporterId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("User cannot upload file to this document."); + } + + if (request.CurrentUser.Role.IsStaff() && document.Department!.Id != request.CurrentUser.Department!.Id) + { + throw new UnauthorizedAccessException("User cannot upload file to this document."); + } + + if (document.File is not null) + { + _context.Files.Remove(document.File); + } + + var file = new FileEntity + { + FileData = request.FileData.ToArray(), + FileType = request.FileType!, + FileExtension = request.FileExtension, + }; + var fileEntity = await _context.Files.AddAsync(file, cancellationToken); + + document.File = fileEntity.Entity; + document.FileId = fileEntity.Entity.Id; + + var result = _context.Documents.Update(document); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From eeae76036e6d92d34b3885cea9f41f680b795489 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 13 Jul 2023 15:35:25 +0700 Subject: [PATCH 145/162] x --- src/Api/appsettings.Development.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 6ede2e2e..de452b22 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -36,7 +36,7 @@ { "Name": "PostgreSQL", "Args": { - "connectionString": "Server=localhost;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured", + "connectionString": "Server=database;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured", "tableName": "Logs", "schemaName": null, "needAutoCreateTable": true, From 2cacc0a7b7fc0353b1454a707480eeefb8c472f9 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 13 Jul 2023 15:35:45 +0700 Subject: [PATCH 146/162] x --- src/Api/appsettings.Development.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index de452b22..b547eab4 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -36,7 +36,7 @@ { "Name": "PostgreSQL", "Args": { - "connectionString": "Server=database;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured", + "connectionString": "Server=postgres;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured", "tableName": "Logs", "schemaName": null, "needAutoCreateTable": true, From 28c3e5736d6843656572878c9bea182c746371fd Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Thu, 27 Jul 2023 08:28:01 +0700 Subject: [PATCH 147/162] Finale/khoa last touch (#382) * refactor: move get all request logs to logging folder * fix: create and get entry now accept unicode * add regex validator for entry name. * fix: file extension when download * fix: stoopid 1 * fix: sorry bao san --------- Co-authored-by: StarryFolf --- src/Api/Controllers/BorrowsController.cs | 1 + src/Application/Entries/Commands/CreateEntry.cs | 5 +++-- src/Application/Entries/Commands/CreateSharedEntry.cs | 2 ++ src/Application/Entries/Commands/DownloadDigitalFile.cs | 2 +- src/Application/Entries/Queries/GetAllEntriesPaginated.cs | 2 +- .../Queries/GetAllRequestLogsPaginated.cs | 3 +-- 6 files changed, 9 insertions(+), 6 deletions(-) rename src/Application/{Borrows => Loggings}/Queries/GetAllRequestLogsPaginated.cs (97%) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 7f49b61e..ce16fece 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -7,6 +7,7 @@ using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; using Application.Identity; +using Application.Loggings.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index e5dcc8f8..1a9ddb4c 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -23,11 +23,12 @@ public Validator() RuleFor(x => x.Name) .NotEmpty().WithMessage("Entry's name is required.") + .Matches("^[\\p{L}A-Za-z_.\\s\\-0-9]*$").WithMessage("Invalid name format.") .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); - + RuleFor(x => x.Path) .NotEmpty().WithMessage("Entry's path is required.") - .Matches("^(/(?!/)[A-Za-z_.\\s\\-0-9]*)+(? x.Name) + .NotEmpty().WithMessage("Entry's name is required.") + .Matches("^[\\p{L}A-Za-z_.\\s\\-0-9]*$").WithMessage("Invalid name format.") .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); } } diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index ef020e9a..85048f97 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -80,7 +80,7 @@ public async Task Handle(Command request, CancellationToken cancellation { Content = content, FileType = fileType, - FileName = entry.Name, + FileName = $"{entry.Name}.{entry.File.FileExtension}", }; } } diff --git a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs index 53dbdfb0..ef0cf431 100644 --- a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs @@ -20,7 +20,7 @@ public Validator() RuleFor(x => x.EntryPath) .NotEmpty().WithMessage("File's path is required.") - .Matches("^(/(?!/)[A-Za-z_.\\s\\-0-9]*)+(? Date: Fri, 28 Jul 2023 00:00:02 +0700 Subject: [PATCH 148/162] add: overdue worker service (#385) * add: overdue worker service * fix: get logs search term * stoopid me --- src/Api/Services/BackgroundWorkers.cs | 25 ++++++++++++++++++- .../Loggings/Queries/GetAllLogsPaginated.cs | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Api/Services/BackgroundWorkers.cs b/src/Api/Services/BackgroundWorkers.cs index 9ae66e23..baf37b11 100644 --- a/src/Api/Services/BackgroundWorkers.cs +++ b/src/Api/Services/BackgroundWorkers.cs @@ -1,4 +1,5 @@ using Application.Common.Interfaces; +using Domain.Statuses; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -20,7 +21,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var workers = new List { DisposeExpiredEntries(TimeSpan.FromSeconds(10), stoppingToken), - DisposeExpiredPermissions(TimeSpan.FromSeconds(10), stoppingToken) + DisposeExpiredPermissions(TimeSpan.FromSeconds(10), stoppingToken), + HandleOverdueRequest(TimeSpan.FromMinutes(2),stoppingToken), }; await Task.WhenAll(workers.ToArray()); @@ -51,4 +53,25 @@ private async Task DisposeExpiredPermissions(TimeSpan delay, CancellationToken s await context.SaveChangesAsync(stoppingToken); await Task.Delay(delay, stoppingToken); } + + private async Task HandleOverdueRequest(TimeSpan delay, CancellationToken stoppingToken) + { + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var overdueRequests = context.Borrows + .Where(x => x.Status != BorrowRequestStatus.Overdue + && x.Status == BorrowRequestStatus.CheckedOut + && x.DueTime < localDateTimeNow) + .ToList(); + + foreach (var request in overdueRequests) + { + request.Status = BorrowRequestStatus.Overdue; + } + context.Borrows.UpdateRange(overdueRequests); + + await context.SaveChangesAsync(stoppingToken); + await Task.Delay(delay, stoppingToken); + } } \ No newline at end of file diff --git a/src/Application/Loggings/Queries/GetAllLogsPaginated.cs b/src/Application/Loggings/Queries/GetAllLogsPaginated.cs index caced59e..9ee82c0d 100644 --- a/src/Application/Loggings/Queries/GetAllLogsPaginated.cs +++ b/src/Application/Loggings/Queries/GetAllLogsPaginated.cs @@ -47,7 +47,7 @@ public async Task> Handle(Query request, CancellationToken if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { - logs = logs.Where(x => x.Message!.Contains(request.SearchTerm.Trim().ToLower())); + logs = logs.Where(x => x.Message!.ToLower().Contains(request.SearchTerm.Trim().ToLower())); } var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; From 8a0811396d65b68d5f873c78ea151dae4d129640 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 28 Jul 2023 00:35:15 +0700 Subject: [PATCH 149/162] fix: document importer can borrow his/her documents now (#384) --- src/Application/Borrows/Commands/BorrowDocument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 507f4420..4ef3dbaf 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -150,7 +150,7 @@ is BorrowRequestStatus.Approved if (document.IsPrivate) { var isGranted = _permissionManager.IsGranted(request.DocumentId, DocumentOperation.Borrow, request.BorrowerId); - if (!isGranted) + if (document.ImporterId != request.BorrowerId && !isGranted) { throw new UnauthorizedAccessException("You don't have permission to borrow this document."); } From 03e3e43b621fe7e7896dd8ba43e6641b5390f6d6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Fri, 28 Jul 2023 01:12:02 +0700 Subject: [PATCH 150/162] ehehe (#386) --- .../Commands/UploadFileToDocument.cs | 7 ++++++ .../Entries/Commands/CreateEntry.cs | 25 ++++++++++++++++--- .../Entries/Queries/GetAllEntriesPaginated.cs | 1 + 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Application/Documents/Commands/UploadFileToDocument.cs b/src/Application/Documents/Commands/UploadFileToDocument.cs index 989114a1..6690469d 100644 --- a/src/Application/Documents/Commands/UploadFileToDocument.cs +++ b/src/Application/Documents/Commands/UploadFileToDocument.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; @@ -5,6 +6,7 @@ using Domain.Entities; using Domain.Entities.Digital; using Domain.Entities.Physical; +using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -62,6 +64,11 @@ public async Task Handle(Command request, CancellationToken cancell _context.Files.Remove(document.File); } + if (document.Status is DocumentStatus.Issued or DocumentStatus.Lost ) + { + throw new ConflictException("This document cannot be uploaded to."); + } + var file = new FileEntity { FileData = request.FileData.ToArray(), diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 1a9ddb4c..3f485f18 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -17,13 +17,33 @@ namespace Application.Entries.Commands; public class CreateEntry { public class Validator : AbstractValidator { + private readonly string[] _allowedExtensions = + { + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "ppt", + "pptx", + }; public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; + RuleFor(x => x.FileExtension) + .Must(extension => + { + if (!_allowedExtensions.Contains(extension)) + { + throw new ConflictException("This file is not supported."); + } + return true; + }); + RuleFor(x => x.Name) .NotEmpty().WithMessage("Entry's name is required.") - .Matches("^[\\p{L}A-Za-z_.\\s\\-0-9]*$").WithMessage("Invalid name format.") + .Matches("^[\\p{L}A-Za-z()_.\\s\\-0-9]*$").WithMessage("Invalid name format.") .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); RuleFor(x => x.Path) @@ -104,9 +124,6 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("File size must be lower than 20MB"); } - var lastDotIndex = request.Name.LastIndexOf(".", StringComparison.Ordinal); - var fileExtension = request.Name.Substring(lastDotIndex + 1, request.Name.Length - lastDotIndex - 1); - var fileEntity = new FileEntity() { FileData = request.FileData.ToArray(), diff --git a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs index ef0cf431..6eb64ff8 100644 --- a/src/Application/Entries/Queries/GetAllEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllEntriesPaginated.cs @@ -48,6 +48,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var entries = _context.Entries + .Include(x => x.File) .Where(x => x.Path.Equals(request.EntryPath) && x.OwnerId == request.CurrentUser.Id); From 639beb328d220471ee5cc98f52fadc977370b3e7 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Fri, 28 Jul 2023 04:05:20 +0700 Subject: [PATCH 151/162] Fix/entry (#383) * fix entry related stuff * a --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- .../Entries/Commands/CreateEntry.cs | 44 ++++++++++++++++--- .../Entries/Commands/MoveEntryToBin.cs | 15 ++----- .../Entries/Commands/RestoreBinEntry.cs | 40 ++++++++--------- .../Entries/Commands/UpdateEntry.cs | 1 + 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 3f485f18..430fb41a 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -82,8 +82,8 @@ public async Task Handle(Command request, CancellationToken cancellati { var baseDirectoryExists = await _context.Entries.AnyAsync( x => request.Path.Trim().ToLower() - .Equals((x.Path.Equals("/") ? (x.Path + x.Name) : (x.Path + "/" + x.Name)).ToLower()) - && x.FileId == null, cancellationToken); + .Equals((x.Path.Equals("/") ? (x.Path + x.Name) : (x.Path + "/" + x.Name)).ToLower()) + && x.FileId == null && x.OwnerId == request.CurrentUser.Id, cancellationToken); if (!request.Path.Equals("/") && !baseDirectoryExists) { @@ -107,9 +107,10 @@ public async Task Handle(Command request, CancellationToken cancellati if (request.IsDirectory) { var entry = await _context.Entries.FirstOrDefaultAsync( - x => x.Name.Trim().Equals(request.Name.Trim()) - && x.Path.Trim().Equals(request.Path.Trim()) - && x.FileId == null, cancellationToken); + x => x.Name.Trim().Equals(request.Name.Trim()) + && x.Path.Trim().Equals(request.Path.Trim()) + && x.FileId == null + && x.OwnerId == request.CurrentUser.Id, cancellationToken); if (entry is not null) { @@ -118,12 +119,43 @@ public async Task Handle(Command request, CancellationToken cancellati } else { - // Make this dynamic + var entries = _context.Entries.AsQueryable() + .Where(x => x.Name.Trim().Substring(0, request.Name.Trim().Length).Equals(request.Name.Trim()) + && x.Path.Trim().Equals(request.Path.Trim()) + && x.FileId != null + && x.OwnerId == request.CurrentUser.Id); + if (request.FileData!.Length > FileUtil.ToByteFromMb(20)) { throw new ConflictException("File size must be lower than 20MB"); } + + if (entries.Any()) + { + var i = 0; + while (true) + { + var temp = ""; + if (i == 0) + { + temp = entryEntity.Name; + } + else + { + temp = $"{entryEntity.Name} ({i})"; + } + var checkEntry = await entries.AnyAsync(x => x.Name.Equals(temp),cancellationToken); + if (!checkEntry) + { + entryEntity.Name = temp; + break; + } + + i++; + } + } + var fileEntity = new FileEntity() { FileData = request.FileData.ToArray(), diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index e83c245b..1361eae9 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -46,15 +46,6 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("Entry does not exist."); } - var entryPath = entry.Path; - var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); - var binCheck = entryPath.Substring(0, firstSlashIndex); - - if (binCheck.Contains(BinString)) - { - throw new ConflictException("Entry is already in bin."); - } - if (!entry.Owner.Id.Equals(request.CurrentUser.Id)) { throw new UnauthorizedAccessException("You do not have permission to move this entry into bin."); @@ -80,10 +71,10 @@ public async Task Handle(Command request, CancellationToken cancellati foreach (var childEntry in childEntries) { var childBinPath = ownerUsername + BinString; - if (!entry.Path.Equals("/")) + if (!childEntry.Path.Equals("/")) { - childBinPath += childEntry.Path[c..]; - } // /x /x abc /x/abc de => _bin + /x abc _bin/x/abc de + childBinPath += $"/{childEntry.Path[c..]}"; + } childEntry.OldPath = childEntry.Path; childEntry.Path = childBinPath; childEntry.LastModified = localDateTimeNow; diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index da924ade..4daf9698 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -37,16 +37,16 @@ public Handler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider public async Task Handle(Command request, CancellationToken cancellationToken) { - var entry = await _context.Entries + var binEntry = await _context.Entries .Include(x => x.Owner) .FirstOrDefaultAsync(x => x.Id == request.EntryId, cancellationToken); - if (entry is null) + if (binEntry is null) { throw new KeyNotFoundException("Entry does not exist."); } - var entryPath = entry.Path; + var entryPath = binEntry.Path; var firstSlashIndex = entryPath.IndexOf("/", StringComparison.Ordinal); var binCheck = firstSlashIndex < 0 ? entryPath : entryPath.Substring(0, firstSlashIndex); @@ -56,7 +56,7 @@ public async Task Handle(Command request, CancellationToken cancellati throw new NotChangedException("Entry is not in bin."); } - if (entry.Owner.Id != request.CurrentUser.Id) + if (binEntry.Owner.Id != request.CurrentUser.Id) { throw new UnauthorizedAccessException("You do not have the permission to restore this entry."); } @@ -66,20 +66,16 @@ public async Task Handle(Command request, CancellationToken cancellati var entryCheckPath = entryPath.Replace(binCheck, ""); var entryCheck = await _context.Entries.FirstOrDefaultAsync(x => - (x.Path.Equals("/") ? x.Path + x.Name : x.Path + "/" + x.Name).Equals(entryCheckPath.Equals("/") ? - entryCheckPath + entry.Name : entryCheckPath + "/" + entry.Name), cancellationToken); + (x.Path.Equals("/") ? x.Path + x.Name : x.Path + "/" + x.Name).Equals(entryCheckPath + "/" + binEntry.Name) + && x.OwnerId == request.CurrentUser.Id + && ((x.FileId != null && binEntry.FileId != null) || (x.FileId == null && binEntry.FileId == null)), cancellationToken); if (entryCheck is not null) { - entryCheck.LastModified = localDateTimeNow; - entryCheck.LastModifiedBy = request.CurrentUser.Id; - var dupeResult = _context.Entries.Update(entryCheck); - _context.Entries.Remove(entry); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(dupeResult.Entity); + throw new ConflictException("Entry already exists outside of bin."); } - var splitPath = entry.OldPath!.Split("/"); + var splitPath = binEntry.OldPath!.Split("/"); var currentPath = "/"; foreach (var node in splitPath) { @@ -116,9 +112,9 @@ public async Task Handle(Command request, CancellationToken cancellati currentPath = currentPath.Equals("/") ? currentPath + node : currentPath + "/" + node; } - if (entry.IsDirectory) + if (binEntry.IsDirectory) { - var path = entry.Path[^1].Equals('/') ? entry.Path + entry.Name : $"{entry.Path}/{entry.Name}"; + var path = binEntry.Path[^1].Equals('/') ? binEntry.Path + binEntry.Name : $"{binEntry.Path}/{binEntry.Name}"; var pattern = $"{path}/%"; var childEntries = _context.Entries.Where(x => x.Path.Trim().Equals(path) @@ -134,16 +130,16 @@ public async Task Handle(Command request, CancellationToken cancellati } } - entry.Path = entry.OldPath; - entry.OldPath = null; - entry.LastModified = localDateTimeNow; - entry.LastModifiedBy = request.CurrentUser.Id; + binEntry.Path = binEntry.OldPath; + binEntry.OldPath = null; + binEntry.LastModified = localDateTimeNow; + binEntry.LastModifiedBy = request.CurrentUser.Id; - var result = _context.Entries.Update(entry); + var result = _context.Entries.Update(binEntry); await _context.SaveChangesAsync(cancellationToken); - using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) + using (Logging.PushProperties(nameof(Entry), binEntry.Id, request.CurrentUser.Id)) { - _logger.LogRestoreBinEntry(request.CurrentUser.Username, entry.Id.ToString()); + _logger.LogRestoreBinEntry(request.CurrentUser.Username, binEntry.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index 7f463cb7..e8c6c007 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -24,6 +24,7 @@ public Validator() RuleFor(x => x.Name) .NotEmpty().WithMessage("Entry's name is required.") + .Matches("^[\\p{L}A-Za-z_.\\s\\-0-9]*$").WithMessage("Invalid name format.") .MaximumLength(256).WithMessage("Name cannot exceed 256 characters."); } } From 41e6a05cba1d70dd29922d47ca34329ad60bc5ca Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 29 Jul 2023 18:21:05 +0700 Subject: [PATCH 152/162] feat: get all shared users for a document (#391) --- src/Api/Controllers/DocumentsController.cs | 27 +++++++ ...sOfASharedEntryPaginatedQueryParameters.cs | 2 +- src/Api/Controllers/SharedController.cs | 2 +- .../Models/Dtos/Physical/PermissionDto.cs | 2 + .../GetAllSharedUsersOfDocumentPaginated.cs | 79 +++++++++++++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/Application/Documents/Queries/GetAllSharedUsersOfDocumentPaginated.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index fda1be1c..b810cacc 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Documents; +using Api.Controllers.Payload.Requests.Users; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; @@ -279,7 +280,33 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Get shared users from a shared document paginated + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{documentId:guid}/shared-users")] + public async Task>> GetSharedUsersByDocumentId( + [FromRoute] Guid documentId, + [FromQuery] GetAllSharedUsersPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllSharedUsersOfDocumentPaginated.Query() + { + CurrentUser = currentUser, + DocumentId = documentId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Download a file linked with a document /// diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs index f2a15b7e..bdb7a102 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs @@ -1,6 +1,6 @@ namespace Api.Controllers.Payload.Requests.Users; -public class GetAllSharedUsersFromASharedEntryPaginatedQueryParameters +public class GetAllSharedUsersPaginatedQueryParameters { /// /// Search term diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs index 29bc6959..165a9979 100644 --- a/src/Api/Controllers/SharedController.cs +++ b/src/Api/Controllers/SharedController.cs @@ -144,7 +144,7 @@ public async Task>> CreateSharedEntry( [HttpGet("entries/{entryId:guid}/shared-users")] public async Task>> GetSharedUsersFromASharedEntryPaginated( [FromRoute] Guid entryId, - [FromQuery] GetAllSharedUsersFromASharedEntryPaginatedQueryParameters queryParameters) + [FromQuery] GetAllSharedUsersPaginatedQueryParameters queryParameters) { var currentUser = _currentUserService.GetCurrentUser(); var query = new GetAllSharedUsersOfASharedEntryPaginated.Query() diff --git a/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs index b9cc456d..c8134c34 100644 --- a/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs @@ -12,6 +12,8 @@ public class PermissionDto : IMapFrom public bool CanBorrow { get; set; } public Guid EmployeeId { get; set; } public Guid DocumentId { get; set; } + + public UserDto Employee { get; set; } = null!; public void Mapping(Profile profile) { diff --git a/src/Application/Documents/Queries/GetAllSharedUsersOfDocumentPaginated.cs b/src/Application/Documents/Queries/GetAllSharedUsersOfDocumentPaginated.cs new file mode 100644 index 00000000..654aae24 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllSharedUsersOfDocumentPaginated.cs @@ -0,0 +1,79 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllSharedUsersOfDocumentPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + } + + public class Handler : IRequestHandler> + { + + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + var permission = await _context.Permissions + .FirstOrDefaultAsync(x => x.DocumentId == request.DocumentId + && x.EmployeeId == request.CurrentUser.Id, cancellationToken); + if ((permission is null || !permission.AllowedOperations.Contains(DocumentOperation.Read.ToString())) + && document.ImporterId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("You do not have permission to view this document shared users."); + } + + var permissions = _context.Permissions + .Include(x => x.Employee) + .Where(x => x.DocumentId == request.DocumentId); + + if (request.SearchTerm is not null && !request.SearchTerm.Trim().Equals(string.Empty)) + { + permissions = permissions.Where(x => + x.Employee.Username.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower()) + || x.Employee.Email.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 10 : request.Size; + + var list = await permissions + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var count = await permissions.CountAsync(cancellationToken); + + var result = _mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file From 468e8d224844079ed8c75b00a62e5240e50a54dd Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 29 Jul 2023 20:31:19 +0700 Subject: [PATCH 153/162] only check extension for files (#388) --- src/Application/Entries/Commands/CreateEntry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 430fb41a..7cb67e6c 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -32,9 +32,9 @@ public Validator() RuleLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.FileExtension) - .Must(extension => + .Must((command, extension) => { - if (!_allowedExtensions.Contains(extension)) + if (!command.IsDirectory && !_allowedExtensions.Contains(extension)) { throw new ConflictException("This file is not supported."); } From 66ccf12288415b2de31497227bfcfd85f8b81433 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sat, 29 Jul 2023 20:37:03 +0700 Subject: [PATCH 154/162] feat: users tracking (#392) --- src/Api/Controllers/DashboardController.cs | 18 ++++++++++++++++++ src/Api/Services/BackgroundWorkers.cs | 15 ++++++++++++++- .../Authorization/RequiresRoleAttribute.cs | 17 +++++++++++------ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 src/Api/Controllers/DashboardController.cs diff --git a/src/Api/Controllers/DashboardController.cs b/src/Api/Controllers/DashboardController.cs new file mode 100644 index 00000000..35ced3e8 --- /dev/null +++ b/src/Api/Controllers/DashboardController.cs @@ -0,0 +1,18 @@ +using Application.Common.Models; +using Application.Identity; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public class DashboardController : ApiControllerBase +{ + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("online-users")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task> GetOnlineUsers() + { + return Ok(Result.Succeed(RequiresRoleAttribute.OnlineUsers.Count)); + } +} \ No newline at end of file diff --git a/src/Api/Services/BackgroundWorkers.cs b/src/Api/Services/BackgroundWorkers.cs index baf37b11..0068d066 100644 --- a/src/Api/Services/BackgroundWorkers.cs +++ b/src/Api/Services/BackgroundWorkers.cs @@ -1,5 +1,6 @@ using Application.Common.Interfaces; using Domain.Statuses; +using Infrastructure.Identity.Authorization; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -22,7 +23,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { DisposeExpiredEntries(TimeSpan.FromSeconds(10), stoppingToken), DisposeExpiredPermissions(TimeSpan.FromSeconds(10), stoppingToken), - HandleOverdueRequest(TimeSpan.FromMinutes(2),stoppingToken), + HandleOverdueRequest(TimeSpan.FromMinutes(2), stoppingToken), + HandleOnlineUsers(TimeSpan.FromSeconds(10), stoppingToken), }; await Task.WhenAll(workers.ToArray()); @@ -74,4 +76,15 @@ private async Task HandleOverdueRequest(TimeSpan delay, CancellationToken stoppi await context.SaveChangesAsync(stoppingToken); await Task.Delay(delay, stoppingToken); } + + private async Task HandleOnlineUsers(TimeSpan delay, CancellationToken stoppingToken) + { + var onlineUsers = RequiresRoleAttribute.OnlineUsers; + foreach (var id in onlineUsers.Keys + .Where(id => DateTime.Now - onlineUsers[id] < delay)) + { + RequiresRoleAttribute.OnlineUsers.Remove(id); + } + await Task.Delay(delay, stoppingToken); + } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs index 469e1e98..a1c63d21 100644 --- a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs +++ b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs @@ -1,5 +1,5 @@ +using System.Collections.Immutable; using System.IdentityModel.Tokens.Jwt; -using Application.Identity; using Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -11,6 +11,7 @@ namespace Infrastructure.Identity.Authorization; public class RequiresRoleAttribute : Attribute, IAuthorizationFilter { private readonly string[] _claimValues; + public static Dictionary OnlineUsers { get; } = new(); public RequiresRoleAttribute(params string[] claimValues) { @@ -25,12 +26,16 @@ public void OnAuthorization(AuthorizationFilterContext context) var user = dbContext.Users.FirstOrDefault(x => x.Email!.Equals(email)); - foreach (var claimValue in _claimValues) + if (user is null) { - if (user!.Role.Equals(claimValue)) - { - return; - } + context.Result = new ForbidResult(); + return; + } + + if (_claimValues.Any(claimValue => user!.Role.Equals(claimValue))) + { + OnlineUsers[user.Id] = DateTime.Now; + return; } context.Result = new ForbidResult(); From 72f287f070ed6fdbd6813b7c7a5b8ec32152192c Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 30 Jul 2023 10:36:00 +0700 Subject: [PATCH 155/162] Feat/online tracking (#394) * feat: users tracking * change: upload file --- src/Application/Documents/Commands/UploadFileToDocument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/Documents/Commands/UploadFileToDocument.cs b/src/Application/Documents/Commands/UploadFileToDocument.cs index 6690469d..75b56197 100644 --- a/src/Application/Documents/Commands/UploadFileToDocument.cs +++ b/src/Application/Documents/Commands/UploadFileToDocument.cs @@ -64,7 +64,7 @@ public async Task Handle(Command request, CancellationToken cancell _context.Files.Remove(document.File); } - if (document.Status is DocumentStatus.Issued or DocumentStatus.Lost ) + if (document.Status is DocumentStatus.Lost ) { throw new ConflictException("This document cannot be uploaded to."); } From 1cf9b1f9deb5f90e4f6cec201d512d72abeddbbc Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 30 Jul 2023 11:04:51 +0700 Subject: [PATCH 156/162] Update/mail (#390) * xxx * xyz * ehehe --- src/Api/appsettings.Development.json | 6 +- .../Borrows/Commands/BorrowDocument.cs | 3 + .../Common/Interfaces/IMailService.cs | 4 +- src/Application/Common/Models/HtmlMailData.cs | 42 ++++++++- .../EventHandlers/RequestCreatedHandler.cs | 37 ++++++++ .../Entries/Commands/ShareEntry.cs | 10 +++ .../EventHandlers/ShareEntryEventHandler.cs | 21 +++++ .../EventHandlers/UserCreatedEventHandler.cs | 2 +- src/Domain/Events/RequestCreated.cs | 24 +++++ src/Domain/Events/ShareEntryEvent.cs | 25 ++++++ src/Infrastructure/ConfigureServices.cs | 2 +- src/Infrastructure/Services/MailService.cs | 90 ++++++++++++++++++- src/Infrastructure/Shared/MailSettings.cs | 10 ++- .../CustomMailService.cs | 14 ++- 14 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 src/Application/Documents/EventHandlers/RequestCreatedHandler.cs create mode 100644 src/Application/Entries/EventHandlers/ShareEntryEventHandler.cs create mode 100644 src/Domain/Events/RequestCreated.cs create mode 100644 src/Domain/Events/ShareEntryEvent.cs diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index b547eab4..7d9b8098 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -18,7 +18,11 @@ "Token": "745f040659edff0ce87b545567da72d2", "SenderName": "ProFile", "SenderEmail": "profile@ezarp.dev", - "TemplateUuid": "9d6a8f25-65e9-4819-be7d-106ce077acf1" + "TemplateUuids": { + "ResetPassword": "9d6a8f25-65e9-4819-be7d-106ce077acf1", + "ShareEntry": "ad69df89-885a-48fb-b8f6-6d06af1a54e3", + "Request": "bce7e60c-d848-4f80-af96-cccd264dcc32" + } }, "Seed": true, "Serilog" : { diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 4ef3dbaf..79a7de68 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -157,7 +157,10 @@ is BorrowRequestStatus.Approved entity.Status = BorrowRequestStatus.Approved; } + var result = await _context.Borrows.AddAsync(entity, cancellationToken); + entity.AddDomainEvent(new RequestCreated($"{user.FirstName} {user.LastName}", "borrow request", "borrow", + document.Title, entity.Id, request.BorrowReason, document.Id)); await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("Request", document.Id, user.Id)) { diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs index 1da7a46d..d02b4ae6 100644 --- a/src/Application/Common/Interfaces/IMailService.cs +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -4,5 +4,7 @@ namespace Application.Common.Interfaces; public interface IMailService { - bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword); + bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash); + bool SendShareEntryHtmlMail(bool isDirectory, string name, string sharerName, string operation, string ownerName, string email, string path); + bool SendCreateRequestHtmlMail(string userName, string requestType, string operation, string documentName, string reason, Guid documentId, string email); } \ No newline at end of file diff --git a/src/Application/Common/Models/HtmlMailData.cs b/src/Application/Common/Models/HtmlMailData.cs index 9fa0959b..105953a9 100644 --- a/src/Application/Common/Models/HtmlMailData.cs +++ b/src/Application/Common/Models/HtmlMailData.cs @@ -11,7 +11,7 @@ public class HtmlMailData [JsonPropertyName("template_uuid")] public string TemplateUuid { get; set; } [JsonPropertyName("template_variables")] - public TemplateVariables TemplateVariables { get; set; } + public object TemplateVariables { get; set; } } public class From @@ -28,12 +28,46 @@ public class To public string Email { get; set; } } -public class TemplateVariables +public class ResetPasswordTemplateVariables { [JsonPropertyName("user_email")] public string UserEmail { get; set; } - [JsonPropertyName("reset_password_token_hash")] - public string ResetPasswordTokenHash { get; set; } + [JsonPropertyName("token_hash")] + public string TokenHash { get; set; } [JsonPropertyName("user_password")] public string UserPassword { get; set; } +} + +public class ShareEntryTemplateVariables +{ + [JsonPropertyName("entry_type")] + public string EntryType { get; set; } + [JsonPropertyName("entry_name")] + public string EntryName { get; set; } + [JsonPropertyName("sharer_name")] + public string SharerName { get; set; } + [JsonPropertyName("operation")] + public string Operation { get; set; } + [JsonPropertyName("owner_name")] + public string OwnerName { get; set; } + [JsonPropertyName("path")] + public string Path { get; set; } +} + +public class CreateRequestTemplateVariables +{ + [JsonPropertyName("user_name")] + public string UserName { get; set; } + [JsonPropertyName("request_type")] + public string RequestType { get; set; } + [JsonPropertyName("operation")] + public string Operation { get; set; } + [JsonPropertyName("document_name")] + public string DocumentName { get; set; } + [JsonPropertyName("reason")] + public string Reason { get; set; } + [JsonPropertyName("path")] + public string Path { get; set; } + [JsonPropertyName("id")] + public string Id { get; set; } } \ No newline at end of file diff --git a/src/Application/Documents/EventHandlers/RequestCreatedHandler.cs b/src/Application/Documents/EventHandlers/RequestCreatedHandler.cs new file mode 100644 index 00000000..fe2cc6ea --- /dev/null +++ b/src/Application/Documents/EventHandlers/RequestCreatedHandler.cs @@ -0,0 +1,37 @@ +using Application.Common.Interfaces; +using Domain.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.EventHandlers; + +public class RequestCreatedHandler : INotificationHandler +{ + private readonly IApplicationDbContext _context; + private readonly IMailService _mailService; + + public RequestCreatedHandler(IMailService mailService, IApplicationDbContext context) + { + _mailService = mailService; + _context = context; + } + + public async Task Handle(RequestCreated notification, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == notification.DocumentId, cancellationToken); + + var departmentId = document!.Department!.Id; + + var staff = await _context.Staffs + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.Room!.DepartmentId == departmentId, cancellationToken); + + if (staff is not null) + { + _mailService.SendCreateRequestHtmlMail(notification.UserName, notification.RequestType, notification.Operation, + notification.DocumentTitle, notification.Reason, notification.RequestId, staff.User.Email); + } + } +} \ No newline at end of file diff --git a/src/Application/Entries/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs index 320454b6..5ca36e64 100644 --- a/src/Application/Entries/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -7,6 +7,7 @@ using AutoMapper; using Domain.Entities; using Domain.Entities.Digital; +using Domain.Events; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -103,6 +104,15 @@ public async Task Handle(Command request, CancellationToken await GrantOrRevokePermission(childEntry, user, childAllowOperations, request.ExpiryDate, false, cancellationToken); } } + + if (request.CanView) + { + var p = "view"; + if (request.CanEdit) p = "edit"; + entry.AddDomainEvent(new ShareEntryEvent(entry.Name, + request.CurrentUser.FirstName + request.CurrentUser.LastName, + entry.Owner.FirstName + entry.Owner.LastName, user.Email, entry.IsDirectory, p, entry.Id.ToString())); + } await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties(nameof(Entry), entry.Id, request.CurrentUser.Id)) { diff --git a/src/Application/Entries/EventHandlers/ShareEntryEventHandler.cs b/src/Application/Entries/EventHandlers/ShareEntryEventHandler.cs new file mode 100644 index 00000000..eeb2a243 --- /dev/null +++ b/src/Application/Entries/EventHandlers/ShareEntryEventHandler.cs @@ -0,0 +1,21 @@ +using Application.Common.Interfaces; +using Domain.Events; +using MediatR; + +namespace Application.Entries.EventHandlers; + +public class ShareEntryEventHandler : INotificationHandler +{ + private readonly IMailService _mailService; + + public ShareEntryEventHandler(IMailService mailService) + { + _mailService = mailService; + } + + public async Task Handle(ShareEntryEvent notification, CancellationToken cancellationToken) + { + _mailService.SendShareEntryHtmlMail(notification.IsDirectory, notification.EntryName, notification.SharerName, + notification.Operation, notification.OwnerName, notification.SharedUserEmail, notification.Path); + } +} \ No newline at end of file diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs index 17538775..c0aed008 100644 --- a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -35,6 +35,6 @@ public async Task Handle(UserCreatedEvent notification, CancellationToken cancel await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken, cancellationToken); await _authDbContext.SaveChangesAsync(cancellationToken); - _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password); + _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, resetPasswordToken.TokenHash); } } \ No newline at end of file diff --git a/src/Domain/Events/RequestCreated.cs b/src/Domain/Events/RequestCreated.cs new file mode 100644 index 00000000..81276057 --- /dev/null +++ b/src/Domain/Events/RequestCreated.cs @@ -0,0 +1,24 @@ +using Domain.Common; + +namespace Domain.Events; + +public class RequestCreated : BaseEvent +{ + public RequestCreated(string userName, string requestType, string operation, string documentTitle, Guid requestId, string reason, Guid documentId) + { + UserName = userName; + RequestType = requestType; + Operation = operation; + DocumentTitle = documentTitle; + RequestId = requestId; + Reason = reason; + DocumentId = documentId; + } + public string UserName { get; } + public string RequestType { get; } + public string Operation { get; } + public string DocumentTitle { get; } + public string Reason { get; } + public Guid DocumentId { get; } + public Guid RequestId { get; } +} \ No newline at end of file diff --git a/src/Domain/Events/ShareEntryEvent.cs b/src/Domain/Events/ShareEntryEvent.cs new file mode 100644 index 00000000..0e2f4a2f --- /dev/null +++ b/src/Domain/Events/ShareEntryEvent.cs @@ -0,0 +1,25 @@ +using Domain.Common; + +namespace Domain.Events; + +public class ShareEntryEvent : BaseEvent +{ + public ShareEntryEvent(string entryName, string sharerName, string ownerName, string sharedUserEmail, bool isDirectory, string operation, string path) + { + EntryName = entryName; + SharerName = sharerName; + OwnerName = ownerName; + SharedUserEmail = sharedUserEmail; + IsDirectory = isDirectory; + Operation = operation; + Path = path; + } + + public string EntryName { get; } + public string SharerName { get; } + public string OwnerName { get; } + public string SharedUserEmail { get; } + public bool IsDirectory { get; } + public string Operation { get; } + public string Path { get; } +} \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 710b935b..dfade6b9 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -108,7 +108,7 @@ private static IServiceCollection AddMailService(this IServiceCollection service options.Token = mailSettings!.Token; options.SenderEmail = mailSettings!.SenderEmail; options.SenderName = mailSettings!.SenderName; - options.TemplateUuid = mailSettings!.TemplateUuid; + options.TemplateUuids = mailSettings!.TemplateUuids; }); services.AddTransient(); diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 62faf977..dc045777 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -17,7 +17,7 @@ public MailService(IOptions mailSettingsOptions) _mailSettings = mailSettingsOptions.Value; } - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash) { var data = new HtmlMailData() { @@ -30,11 +30,95 @@ public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword { new (){ Email = userEmail }, }, - TemplateUuid = _mailSettings.TemplateUuid, - TemplateVariables = new TemplateVariables() + TemplateUuid = _mailSettings.TemplateUuids.ResetPassword, + TemplateVariables = new ResetPasswordTemplateVariables() { UserEmail = userEmail, UserPassword = temporaryPassword, + TokenHash = tokenHash + }, + }; + + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + var client = new RestClient(_mailSettings.ClientUrl); + var request = new RestRequest + { + Method = Method.Post + }; + + request.AddHeader("Authorization", $"{JwtBearerDefaults.AuthenticationScheme} {_mailSettings.Token}"); + request.AddHeader("Content-Type", "application/json"); + request.AddParameter("application/json", json, ParameterType.RequestBody); + var response = client.Execute(request); + return response.IsSuccessStatusCode; + } + + public bool SendShareEntryHtmlMail(bool isDirectory, string name, string sharerName, string operation, string ownerName, + string email, string path) + { + var data = new HtmlMailData() + { + From = new From() + { + Email = _mailSettings.SenderEmail, + Name = _mailSettings.SenderName, + }, + To = new To[] + { + new (){ Email = email }, + }, + TemplateUuid = _mailSettings.TemplateUuids.ShareEntry, + TemplateVariables = new ShareEntryTemplateVariables() + { + EntryName = name, + Operation = operation, + EntryType = isDirectory ? "Folder" : "File", + OwnerName = ownerName, + SharerName = sharerName, + Path = path + }, + }; + + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + var client = new RestClient(_mailSettings.ClientUrl); + var request = new RestRequest + { + Method = Method.Post + }; + + request.AddHeader("Authorization", $"{JwtBearerDefaults.AuthenticationScheme} {_mailSettings.Token}"); + request.AddHeader("Content-Type", "application/json"); + request.AddParameter("application/json", json, ParameterType.RequestBody); + var response = client.Execute(request); + return response.IsSuccessStatusCode; + } + + public bool SendCreateRequestHtmlMail(string userName, string requestType, string operation, string documentName, + string reason, Guid documentId, string email) + { + var data = new HtmlMailData() + { + From = new From() + { + Email = _mailSettings.SenderEmail, + Name = _mailSettings.SenderName, + }, + To = new To[] + { + new (){ Email = email }, + }, + TemplateUuid = _mailSettings.TemplateUuids.Request, + TemplateVariables = new CreateRequestTemplateVariables() + { + Operation = operation, + Reason = reason, + DocumentName = documentName, + UserName = userName, + Id = documentId.ToString(), + RequestType = requestType, + Path = !requestType.Equals("borrow request") ? "import/manage" : "requests" }, }; diff --git a/src/Infrastructure/Shared/MailSettings.cs b/src/Infrastructure/Shared/MailSettings.cs index 2e139992..da97ad6b 100644 --- a/src/Infrastructure/Shared/MailSettings.cs +++ b/src/Infrastructure/Shared/MailSettings.cs @@ -6,5 +6,13 @@ public class MailSettings public string Token { get; set; } public string SenderName { get; set; } public string SenderEmail { get; set; } - public string TemplateUuid { get; set; } + + public Template TemplateUuids { get; set; } +} + +public class Template +{ + public string ResetPassword { get; set; } + public string ShareEntry { get; set; } + public string Request { get; set; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs index aaeabc16..3327efea 100644 --- a/tests/Application.Tests.Integration/CustomMailService.cs +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -4,7 +4,19 @@ namespace Application.Tests.Integration; public class CustomMailService : IMailService { - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash) + { + return true; + } + + public bool SendShareEntryHtmlMail(bool isDirectory, string name, string sharerName, string operation, string ownerName, + string email, string path) + { + return true; + } + + public bool SendCreateRequestHtmlMail(string userName, string requestType, string operation, string documentName, + string reason, Guid documentId, string email) { return true; } From d84862a0f6f9bde540307b447c004d099dba178c Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Sun, 30 Jul 2023 17:17:05 +0700 Subject: [PATCH 157/162] Feat/dashboard (#395) * add: dashboard controller * Feat/import document number (#393) * add: metric for import document count * log fucked up * minor stuff * get login users metric * stoopid 2 * minor changes * add: get user with largest drive * fix: appsettings * i --------- Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> --- src/Api/Controllers/AuthController.cs | 13 +++- src/Api/Controllers/DashboardController.cs | 60 +++++++++++++++ .../GetImportedDocumentsMetricsRequest.cs | 9 +++ .../Commands/ApproveOrRejectBorrowRequest.cs | 4 +- .../Borrows/Commands/BorrowDocument.cs | 2 +- .../Borrows/Commands/CancelBorrowRequest.cs | 2 +- .../Borrows/Commands/CheckoutDocument.cs | 2 +- .../Borrows/Commands/ReturnDocument.cs | 2 +- .../Borrows/Commands/UpdateBorrow.cs | 2 +- .../Logging}/BorrowLogExtensions.cs | 2 +- .../Logging}/DocumentLogExtensions.cs | 2 +- .../Extensions/Logging}/EntryLogExtension.cs | 2 +- .../Extensions/Logging}/FolderLogExtension.cs | 2 +- .../Logging}/ImportRequestLogExtensions.cs | 3 +- .../Extensions/Logging}/LockerLogExtension.cs | 2 +- .../Extensions/Logging/LoginLogExtension.cs | 11 +++ .../Extensions/Logging}/RoomLogExtension.cs | 2 +- .../Extensions/Logging}/StaffLogExtensions.cs | 2 +- .../Extensions/Logging}/UserLogExtensions.cs | 2 +- .../Common/Messages/LoginLogMessages.cs | 6 ++ .../Models/Dtos/DashBoard/LargestDriveDto.cs | 10 +++ .../Models/Dtos/DashBoard/MetricResultDto.cs | 7 ++ .../Dashboards/Queries/GetImportDocuments.cs | 74 +++++++++++++++++++ .../Dashboards/Queries/GetLoggedInUser.cs | 55 ++++++++++++++ .../Queries/GetUserWithLargestDriveData.cs | 60 +++++++++++++++ .../Documents/Commands/DeleteDocument.cs | 1 + .../Documents/Commands/ImportDocument.cs | 1 + .../Documents/Commands/ShareDocument.cs | 1 + .../Documents/Commands/UpdateDocument.cs | 1 + .../Entries/Commands/CreateEntry.cs | 1 + .../Entries/Commands/CreateSharedEntry.cs | 1 + .../Entries/Commands/DeleteBinEntry.cs | 1 + .../Entries/Commands/DownloadDigitalFile.cs | 1 + .../Entries/Commands/MoveEntryToBin.cs | 1 + .../Entries/Commands/RestoreBinEntry.cs | 1 + .../Entries/Commands/ShareEntry.cs | 1 + .../Entries/Commands/UpdateEntry.cs | 1 + src/Application/Folders/Commands/AddFolder.cs | 1 + .../Folders/Commands/RemoveFolder.cs | 1 + .../Folders/Commands/UpdateFolder.cs | 1 + .../Commands/ApproveOrRejectDocument.cs | 1 + .../ImportRequests/Commands/AssignDocument.cs | 1 + .../Commands/CheckinDocument.cs | 1 + .../Commands/RequestImportDocument.cs | 1 + src/Application/Lockers/Commands/AddLocker.cs | 1 + .../Lockers/Commands/RemoveLocker.cs | 1 + .../Lockers/Commands/UpdateLocker.cs | 1 + src/Application/Rooms/Commands/AddRoom.cs | 1 + src/Application/Rooms/Commands/RemoveRoom.cs | 1 + src/Application/Rooms/Commands/UpdateRoom.cs | 1 + .../Staffs/Commands/AssignStaff.cs | 1 + .../Staffs/Commands/RemoveStaffFromRoom.cs | 1 + src/Application/Users/Commands/AddUser.cs | 1 + src/Application/Users/Commands/UpdateUser.cs | 1 + 54 files changed, 349 insertions(+), 18 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Dashboard/GetImportedDocumentsMetricsRequest.cs rename src/Application/{Borrows => Common/Extensions/Logging}/BorrowLogExtensions.cs (97%) rename src/Application/{Documents => Common/Extensions/Logging}/DocumentLogExtensions.cs (96%) rename src/Application/{Entries => Common/Extensions/Logging}/EntryLogExtension.cs (97%) rename src/Application/{Folders => Common/Extensions/Logging}/FolderLogExtension.cs (94%) rename src/Application/{ImportRequests => Common/Extensions/Logging}/ImportRequestLogExtensions.cs (97%) rename src/Application/{Lockers => Common/Extensions/Logging}/LockerLogExtension.cs (94%) create mode 100644 src/Application/Common/Extensions/Logging/LoginLogExtension.cs rename src/Application/{Rooms => Common/Extensions/Logging}/RoomLogExtension.cs (94%) rename src/Application/{Staffs => Common/Extensions/Logging}/StaffLogExtensions.cs (92%) rename src/Application/{Users => Common/Extensions/Logging}/UserLogExtensions.cs (90%) create mode 100644 src/Application/Common/Messages/LoginLogMessages.cs create mode 100644 src/Application/Common/Models/Dtos/DashBoard/LargestDriveDto.cs create mode 100644 src/Application/Common/Models/Dtos/DashBoard/MetricResultDto.cs create mode 100644 src/Application/Dashboards/Queries/GetImportDocuments.cs create mode 100644 src/Application/Dashboards/Queries/GetLoggedInUser.cs create mode 100644 src/Application/Dashboards/Queries/GetUserWithLargestDriveData.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 51c3d93c..c4e980c8 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -2,7 +2,9 @@ using System.Security.Authentication; using Api.Controllers.Payload.Requests.Auth; using Api.Controllers.Payload.Responses; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; +using Application.Common.Logging; using Application.Common.Models; using Application.Common.Models.Dtos; using Application.Users.Queries; @@ -18,10 +20,14 @@ namespace Api.Controllers; public class AuthController : ControllerBase { private readonly IIdentityService _identityService; + private readonly ILogger _logger; + private readonly IDateTimeProvider _dateTimeProvider; - public AuthController(IIdentityService identityService) + public AuthController(IIdentityService identityService, ILogger logger, IDateTimeProvider dateTimeProvider) { _identityService = identityService; + _logger = logger; + _dateTimeProvider = dateTimeProvider; } /// @@ -54,6 +60,11 @@ public async Task Login([FromBody] LoginModel loginModel) LastName = loginSuccess.UserCredentials.LastName, }; + var dateTimeNow = _dateTimeProvider.DateTimeNow; + using (Logging.PushProperties("Login", loginResult.Id, loginResult.Id)) + { + _logger.LogLogin(loginResult.Username, dateTimeNow.ToString("yyyy-MM-dd HH:mm:ss")); + } return Ok(Result.Succeed(loginResult)); }, token => diff --git a/src/Api/Controllers/DashboardController.cs b/src/Api/Controllers/DashboardController.cs index 35ced3e8..cc1c7428 100644 --- a/src/Api/Controllers/DashboardController.cs +++ b/src/Api/Controllers/DashboardController.cs @@ -1,4 +1,8 @@ +using Api.Controllers.Payload.Requests.Dashboard; +using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.DashBoard; +using Application.Dashboards.Queries; using Application.Identity; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -7,6 +11,62 @@ namespace Api.Controllers; public class DashboardController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public DashboardController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPost("import-documents")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>>> GetImportDocuments( + [FromBody] GetImportedDocumentsMetricsRequest request) + { + var query = new GetImportDocuments.Query() + { + StartDate = request.StartDate, + EndDate = request.EndDate + }; + + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPost("logins")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetLoggedInUsers( + [FromBody] DateTime date) + { + var query = new GetLoggedInUser.Query() + { + Date = date + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + [RequiresRole(IdentityData.Roles.Admin)] + [HttpPost("largest-drive")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetUserWithLargestDrive( + [FromBody] DateTime date) + { + var query = new GetUserWithLargestDriveData.Query() + { + Date = date + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("online-users")] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/src/Api/Controllers/Payload/Requests/Dashboard/GetImportedDocumentsMetricsRequest.cs b/src/Api/Controllers/Payload/Requests/Dashboard/GetImportedDocumentsMetricsRequest.cs new file mode 100644 index 00000000..95283540 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Dashboard/GetImportedDocumentsMetricsRequest.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Dashboard; + +public class GetImportedDocumentsMetricsRequest +{ + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index ec9397ac..60a9e295 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -120,7 +120,7 @@ is BorrowRequestStatus.Approved using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) { - _logger.LogApproveBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogApproveBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } } @@ -130,7 +130,7 @@ is BorrowRequestStatus.Approved using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) { - _logger.LogRejectBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogRejectBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 79a7de68..7c7eddad 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -164,7 +164,7 @@ is BorrowRequestStatus.Approved await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("Request", document.Id, user.Id)) { - _logger.LogBorrowDocument(document.Id.ToString(), result.Entity.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogBorrowDocument(_logger, document.Id.ToString(), result.Entity.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index 6f9ffd79..135d05ef 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -65,7 +65,7 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId)) { - _logger.LogCancelBorrowRequest(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogCancelBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 275b344f..0dd95d0a 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -90,7 +90,7 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentStaff.Id)) { - _logger.LogCheckoutDocument(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogCheckoutDocument(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index d2e3cc37..2ee6da97 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -91,7 +91,7 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) { - _logger.LogReturnDocument(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogReturnDocument(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index 0d1d9512..cdbf0d9e 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -115,7 +115,7 @@ is BorrowRequestStatus.Approved using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id)) { - _logger.LogUpdateBorrow(borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); + Common.Extensions.Logging.BorrowLogExtensions.LogUpdateBorrow(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString()); } return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/BorrowLogExtensions.cs b/src/Application/Common/Extensions/Logging/BorrowLogExtensions.cs similarity index 97% rename from src/Application/Borrows/BorrowLogExtensions.cs rename to src/Application/Common/Extensions/Logging/BorrowLogExtensions.cs index 04df6443..cd33de16 100644 --- a/src/Application/Borrows/BorrowLogExtensions.cs +++ b/src/Application/Common/Extensions/Logging/BorrowLogExtensions.cs @@ -1,7 +1,7 @@ using Application.Common.Messages; using Microsoft.Extensions.Logging; -namespace Application.Borrows; +namespace Application.Common.Extensions.Logging; public static partial class BorrowLogExtensions { diff --git a/src/Application/Documents/DocumentLogExtensions.cs b/src/Application/Common/Extensions/Logging/DocumentLogExtensions.cs similarity index 96% rename from src/Application/Documents/DocumentLogExtensions.cs rename to src/Application/Common/Extensions/Logging/DocumentLogExtensions.cs index 790b5479..13e8a07b 100644 --- a/src/Application/Documents/DocumentLogExtensions.cs +++ b/src/Application/Common/Extensions/Logging/DocumentLogExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Documents; +namespace Application.Common.Extensions.Logging; public static partial class DocumentLogExtensions { diff --git a/src/Application/Entries/EntryLogExtension.cs b/src/Application/Common/Extensions/Logging/EntryLogExtension.cs similarity index 97% rename from src/Application/Entries/EntryLogExtension.cs rename to src/Application/Common/Extensions/Logging/EntryLogExtension.cs index e2f850b4..d99b237e 100644 --- a/src/Application/Entries/EntryLogExtension.cs +++ b/src/Application/Common/Extensions/Logging/EntryLogExtension.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Entries; +namespace Application.Common.Extensions.Logging; public static partial class EntryLogExtension { diff --git a/src/Application/Folders/FolderLogExtension.cs b/src/Application/Common/Extensions/Logging/FolderLogExtension.cs similarity index 94% rename from src/Application/Folders/FolderLogExtension.cs rename to src/Application/Common/Extensions/Logging/FolderLogExtension.cs index aca354d6..e5da414c 100644 --- a/src/Application/Folders/FolderLogExtension.cs +++ b/src/Application/Common/Extensions/Logging/FolderLogExtension.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Folders; +namespace Application.Common.Extensions.Logging; public static partial class FolderLogExtension { diff --git a/src/Application/ImportRequests/ImportRequestLogExtensions.cs b/src/Application/Common/Extensions/Logging/ImportRequestLogExtensions.cs similarity index 97% rename from src/Application/ImportRequests/ImportRequestLogExtensions.cs rename to src/Application/Common/Extensions/Logging/ImportRequestLogExtensions.cs index 6719dde6..5143dc19 100644 --- a/src/Application/ImportRequests/ImportRequestLogExtensions.cs +++ b/src/Application/Common/Extensions/Logging/ImportRequestLogExtensions.cs @@ -1,9 +1,8 @@ using Application.Common.Messages; -using Domain.Entities.Physical; using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.ImportRequests; +namespace Application.Common.Extensions.Logging; public static partial class ImportRequestLogExtensions { // Approve or reject document diff --git a/src/Application/Lockers/LockerLogExtension.cs b/src/Application/Common/Extensions/Logging/LockerLogExtension.cs similarity index 94% rename from src/Application/Lockers/LockerLogExtension.cs rename to src/Application/Common/Extensions/Logging/LockerLogExtension.cs index f38951ce..db6d4505 100644 --- a/src/Application/Lockers/LockerLogExtension.cs +++ b/src/Application/Common/Extensions/Logging/LockerLogExtension.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Lockers; +namespace Application.Common.Extensions.Logging; public static partial class LockerLogExtension { diff --git a/src/Application/Common/Extensions/Logging/LoginLogExtension.cs b/src/Application/Common/Extensions/Logging/LoginLogExtension.cs new file mode 100644 index 00000000..0bd03761 --- /dev/null +++ b/src/Application/Common/Extensions/Logging/LoginLogExtension.cs @@ -0,0 +1,11 @@ +using Application.Common.Messages; +using Microsoft.Extensions.Logging; +using EventId = Application.Common.Logging.EventId; + +namespace Application.Common.Extensions.Logging; + +public static partial class LoginLogExtension +{ + [LoggerMessage(Level = LogLevel.Information, Message = LoginLogMessages.Login, EventId = EventId.Approve)] + public static partial void LogLogin(this ILogger logger, string username, string time); +} \ No newline at end of file diff --git a/src/Application/Rooms/RoomLogExtension.cs b/src/Application/Common/Extensions/Logging/RoomLogExtension.cs similarity index 94% rename from src/Application/Rooms/RoomLogExtension.cs rename to src/Application/Common/Extensions/Logging/RoomLogExtension.cs index 13d5f9fe..d1cd8f9f 100644 --- a/src/Application/Rooms/RoomLogExtension.cs +++ b/src/Application/Common/Extensions/Logging/RoomLogExtension.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Rooms; +namespace Application.Common.Extensions.Logging; public static partial class RoomLogExtension { [LoggerMessage(Level = LogLevel.Information, Message = RoomLogMessage.Add, EventId = EventId.Add)] diff --git a/src/Application/Staffs/StaffLogExtensions.cs b/src/Application/Common/Extensions/Logging/StaffLogExtensions.cs similarity index 92% rename from src/Application/Staffs/StaffLogExtensions.cs rename to src/Application/Common/Extensions/Logging/StaffLogExtensions.cs index 8577103e..071fd548 100644 --- a/src/Application/Staffs/StaffLogExtensions.cs +++ b/src/Application/Common/Extensions/Logging/StaffLogExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using EventId = Application.Common.Logging.EventId; -namespace Application.Staffs; +namespace Application.Common.Extensions.Logging; public static partial class StaffLogExtensions { [LoggerMessage(Level = LogLevel.Information, Message = UserLogMessages.Staff.AssignStaff, EventId = EventId.Add)] diff --git a/src/Application/Users/UserLogExtensions.cs b/src/Application/Common/Extensions/Logging/UserLogExtensions.cs similarity index 90% rename from src/Application/Users/UserLogExtensions.cs rename to src/Application/Common/Extensions/Logging/UserLogExtensions.cs index 7607bc02..7dd99cc7 100644 --- a/src/Application/Users/UserLogExtensions.cs +++ b/src/Application/Common/Extensions/Logging/UserLogExtensions.cs @@ -1,7 +1,7 @@ using Application.Common.Messages; using Microsoft.Extensions.Logging; -namespace Application.Users; +namespace Application.Common.Extensions.Logging; public static partial class UserLogExtensions { diff --git a/src/Application/Common/Messages/LoginLogMessages.cs b/src/Application/Common/Messages/LoginLogMessages.cs new file mode 100644 index 00000000..102e3e4b --- /dev/null +++ b/src/Application/Common/Messages/LoginLogMessages.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Messages; + +public class LoginLogMessages +{ + public const string Login = "User with username {Username} Log in at {Time}"; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/DashBoard/LargestDriveDto.cs b/src/Application/Common/Models/Dtos/DashBoard/LargestDriveDto.cs new file mode 100644 index 00000000..b7ce4c77 --- /dev/null +++ b/src/Application/Common/Models/Dtos/DashBoard/LargestDriveDto.cs @@ -0,0 +1,10 @@ +using Application.Users.Queries; + +namespace Application.Common.Models.Dtos.DashBoard; + +public class LargestDriveDto +{ + public string Label { get; set; } + public long Value { get; set; } + public UserDto User { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/DashBoard/MetricResultDto.cs b/src/Application/Common/Models/Dtos/DashBoard/MetricResultDto.cs new file mode 100644 index 00000000..0c98f1e2 --- /dev/null +++ b/src/Application/Common/Models/Dtos/DashBoard/MetricResultDto.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Models.Dtos.DashBoard; + +public class MetricResultDto +{ + public string Label { get; set; } + public int Value { get; set; } +} \ No newline at end of file diff --git a/src/Application/Dashboards/Queries/GetImportDocuments.cs b/src/Application/Dashboards/Queries/GetImportDocuments.cs new file mode 100644 index 00000000..1c4b12cd --- /dev/null +++ b/src/Application/Dashboards/Queries/GetImportDocuments.cs @@ -0,0 +1,74 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.DashBoard; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Dashboards.Queries; + +public class GetImportDocuments +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.StartDate) + .NotEmpty().WithMessage("Start date can not be empty."); + + RuleFor(x => x.EndDate) + .NotEmpty().WithMessage("End date can not be empty."); + } + } + public record Query : IRequest> + { + public DateTime StartDate { get; init; } + public DateTime EndDate { get; init; } + + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var result = new List(); + + var currentDate = request.StartDate.Date; + var endDate = request.EndDate.Date; + + while (currentDate <= endDate) + { + var nextDate = currentDate.AddDays(1); + var label = $"{currentDate:dd-MM} to {nextDate:dd-MM}"; + if (nextDate <= endDate) + { + var date = currentDate; + var importedDocumentsCount = await _context.Documents + .Where(x => x.Created >= LocalDateTime.FromDateTime(date) + && x.Created <= LocalDateTime.FromDateTime(nextDate) + && x.Status != DocumentStatus.Issued) + .CountAsync(cancellationToken); + + result.Add(new MetricResultDto() + { + Label = label, + Value = importedDocumentsCount + }); + } + currentDate = nextDate; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Application/Dashboards/Queries/GetLoggedInUser.cs b/src/Application/Dashboards/Queries/GetLoggedInUser.cs new file mode 100644 index 00000000..0abd755e --- /dev/null +++ b/src/Application/Dashboards/Queries/GetLoggedInUser.cs @@ -0,0 +1,55 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.DashBoard; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Dashboards.Queries; + +public class GetLoggedInUser +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Date) + .NotEmpty().WithMessage("Date can not be empty."); + } + } + public record Query : IRequest + { + public DateTime Date { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public QueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var currentDate = request.Date.Date; + + var label = $"{currentDate:dd-MM}"; + var loggedinUsersCount = await _context.Logs + .Where(x => x.ObjectType!.Equals("Login") + && x.Time >= Instant.FromDateTimeUtc(currentDate) + && x.Time < Instant.FromDateTimeUtc(currentDate.AddDays(1))) + .CountAsync(cancellationToken); + + return new MetricResultDto() + { + Label = label, + Value = loggedinUsersCount + }; + } + } +} \ No newline at end of file diff --git a/src/Application/Dashboards/Queries/GetUserWithLargestDriveData.cs b/src/Application/Dashboards/Queries/GetUserWithLargestDriveData.cs new file mode 100644 index 00000000..d5a2be1e --- /dev/null +++ b/src/Application/Dashboards/Queries/GetUserWithLargestDriveData.cs @@ -0,0 +1,60 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.DashBoard; +using Application.Users.Queries; +using AutoMapper; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Dashboards.Queries; + +public class GetUserWithLargestDriveData +{ + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Date) + .NotEmpty().WithMessage("Date can not be empty."); + } + } + public record Query : IRequest + { + public DateTime Date { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var currentDate = request.Date.Date; + var nextDate = currentDate.AddDays(1); + + var usersWithFileData = await _context.Entries + .Where(x => x.FileId != null && x.Created >= LocalDateTime.FromDateTime(currentDate) + && x.Created < LocalDateTime.FromDateTime(nextDate)) + .GroupBy(entry => entry.Uploader) + .Select(group => new LargestDriveDto() + { + User = _mapper.Map(group.Key), + Label = $"{currentDate:dd-MM}", + Value = group.Sum(x => x.File!.FileData.Length) + }) + .OrderByDescending(x => x.Value) + .FirstOrDefaultAsync(cancellationToken); + return usersWithFileData; + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 176a6845..6331ff18 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,3 +1,4 @@ +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index b105ff93..9a3ac6ee 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs index c1359f80..9fd5e7bb 100644 --- a/src/Application/Documents/Commands/ShareDocument.cs +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index fc8e5aea..c67a6cdd 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 7cb67e6c..86647ee2 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index 5fe9d8f0..ebefb8b7 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/DeleteBinEntry.cs b/src/Application/Entries/Commands/DeleteBinEntry.cs index 10379f82..0b4ff8cb 100644 --- a/src/Application/Entries/Commands/DeleteBinEntry.cs +++ b/src/Application/Entries/Commands/DeleteBinEntry.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/DownloadDigitalFile.cs b/src/Application/Entries/Commands/DownloadDigitalFile.cs index 85048f97..88ecfe3f 100644 --- a/src/Application/Entries/Commands/DownloadDigitalFile.cs +++ b/src/Application/Entries/Commands/DownloadDigitalFile.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Operations; diff --git a/src/Application/Entries/Commands/MoveEntryToBin.cs b/src/Application/Entries/Commands/MoveEntryToBin.cs index 1361eae9..4ec05da9 100644 --- a/src/Application/Entries/Commands/MoveEntryToBin.cs +++ b/src/Application/Entries/Commands/MoveEntryToBin.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/RestoreBinEntry.cs b/src/Application/Entries/Commands/RestoreBinEntry.cs index 4daf9698..413ebab5 100644 --- a/src/Application/Entries/Commands/RestoreBinEntry.cs +++ b/src/Application/Entries/Commands/RestoreBinEntry.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/ShareEntry.cs b/src/Application/Entries/Commands/ShareEntry.cs index 5ca36e64..6d6deb73 100644 --- a/src/Application/Entries/Commands/ShareEntry.cs +++ b/src/Application/Entries/Commands/ShareEntry.cs @@ -1,5 +1,6 @@ using System.Configuration; using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Entries/Commands/UpdateEntry.cs b/src/Application/Entries/Commands/UpdateEntry.cs index e8c6c007..f4571d80 100644 --- a/src/Application/Entries/Commands/UpdateEntry.cs +++ b/src/Application/Entries/Commands/UpdateEntry.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Digital; diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 2810e5f0..59bc069c 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index d9d24596..557fe4c4 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index ce5c058d..8338cebb 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index 9beb97d5..f93c2dfa 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.ImportDocument; diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index 1397f4b5..e134f927 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.ImportDocument; diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index 39b78f6c..fa30b592 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 2e768164..ef1b088e 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.ImportDocument; diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 41c558e6..f2123351 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index 9860aea9..f5c3cb55 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 45b5aed9..d4db5580 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index 9dff5688..e6692789 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 4619d011..73d7579c 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index b630cdaa..0cdea5ff 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Staffs/Commands/AssignStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs index e8dd03ef..32f99ee9 100644 --- a/src/Application/Staffs/Commands/AssignStaff.cs +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index c87aec9f..fdd95573 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Models.Dtos.Physical; diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 269c3d91..17dd5b03 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,5 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Helpers; diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 805b0202..584f57d2 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,4 +1,5 @@ using Application.Common.Extensions; +using Application.Common.Extensions.Logging; using Application.Common.Interfaces; using Application.Common.Logging; using Application.Common.Messages; From c9f4e7e988c9f2ad6be97ddfb7454d29d0b634ec Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sun, 30 Jul 2023 17:18:45 +0700 Subject: [PATCH 158/162] feat: found a lost document (#389) --- src/Api/Controllers/BorrowsController.cs | 21 ++++ .../Borrows/Commands/ReportFoundDocument.cs | 106 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/Application/Borrows/Commands/ReportFoundDocument.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index ce16fece..43379604 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -252,6 +252,27 @@ public async Task>> LostReport([FromRoute] Guid b return Ok(Result.Succeed(result)); } + /// + /// Report found document + /// + /// Id of the borrow request to be reported + /// + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("found/{borrowId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> FoundReport([FromRoute] Guid borrowId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new ReportFoundDocument.Command() + { + CurrentUser = currentUser, + BorrowId = borrowId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// Get all logs related to requests. /// diff --git a/src/Application/Borrows/Commands/ReportFoundDocument.cs b/src/Application/Borrows/Commands/ReportFoundDocument.cs new file mode 100644 index 00000000..1ccb64c9 --- /dev/null +++ b/src/Application/Borrows/Commands/ReportFoundDocument.cs @@ -0,0 +1,106 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Borrows.Commands; + +public class ReportFoundDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid BorrowId { get; init; } + + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var borrowRequest = await _context.Borrows + .Include(x => x.Borrower) + .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + + if (borrowRequest is null) + { + throw new KeyNotFoundException("Borrow request does not exist."); + } + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + // Staff cant report lost documents from other department + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + if (borrowRequest.Document.Status is not DocumentStatus.Lost) + { + throw new ConflictException("Document is not lost."); + } + + if (borrowRequest.Status is not BorrowRequestStatus.Lost) + { + throw new ConflictException("Request is not lost."); + } + + // Get borrows for the lost document which are not processable + var borrowsForDocument = _context.Borrows + .Include(x => x.Document) + .Where( x => x.Id != request.BorrowId + && x.Document.Id == borrowRequest.Document.Id + && x.Status == BorrowRequestStatus.NotProcessable); + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + foreach (var borrow in borrowsForDocument) + { + if (borrow.DueTime < localDateTimeNow) + { + borrow.Status = BorrowRequestStatus.Pending; + } + } + + borrowRequest.Status = BorrowRequestStatus.Returned; + borrowRequest.Document.Status = DocumentStatus.Available; + borrowRequest.ActualReturnTime = localDateTimeNow; + + var result = _context.Borrows.Update(borrowRequest); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From b2e7c99f730a6cb3012a75505a19cdc3445c5e45 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Sun, 30 Jul 2023 21:30:24 +0700 Subject: [PATCH 159/162] fix: include issuer (#396) --- src/Application/ImportRequests/Queries/GetImportRequestById.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Application/ImportRequests/Queries/GetImportRequestById.cs b/src/Application/ImportRequests/Queries/GetImportRequestById.cs index 625400cd..76fe5798 100644 --- a/src/Application/ImportRequests/Queries/GetImportRequestById.cs +++ b/src/Application/ImportRequests/Queries/GetImportRequestById.cs @@ -31,6 +31,7 @@ public async Task Handle(Query request, CancellationToken canc { var importRequest = await _context.ImportRequests .Include(x => x.Document) + .ThenInclude(x => x.Importer) .Include(x => x.Room) .FirstOrDefaultAsync(x => x.Id.Equals(request.RequestId), cancellationToken); From 514750f973daa60514c4153a8e4034193a82cb4b Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sun, 30 Jul 2023 23:54:54 +0700 Subject: [PATCH 160/162] update: mail and log --- .../Common/Interfaces/IMailService.cs | 2 +- .../Common/Messages/EntryLogMessages.cs | 2 +- src/Application/Common/Models/HtmlMailData.cs | 4 +-- .../Entries/Commands/CreateEntry.cs | 13 ++------ .../Entries/Commands/CreateSharedEntry.cs | 31 ++++++++++++++++--- .../Queries/GetAllSharedEntriesPaginated.cs | 2 ++ .../Entries/Queries/GetSharedEntryById.cs | 2 ++ .../EventHandlers/UserCreatedEventHandler.cs | 2 +- src/Infrastructure/Services/MailService.cs | 4 +-- .../CustomMailService.cs | 2 +- 10 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs index d02b4ae6..fd24066d 100644 --- a/src/Application/Common/Interfaces/IMailService.cs +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -4,7 +4,7 @@ namespace Application.Common.Interfaces; public interface IMailService { - bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash); + bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string token); bool SendShareEntryHtmlMail(bool isDirectory, string name, string sharerName, string operation, string ownerName, string email, string path); bool SendCreateRequestHtmlMail(string userName, string requestType, string operation, string documentName, string reason, Guid documentId, string email); } \ No newline at end of file diff --git a/src/Application/Common/Messages/EntryLogMessages.cs b/src/Application/Common/Messages/EntryLogMessages.cs index 19bc5a77..f4096fc6 100644 --- a/src/Application/Common/Messages/EntryLogMessages.cs +++ b/src/Application/Common/Messages/EntryLogMessages.cs @@ -9,5 +9,5 @@ public class EntryLogMessages public const string ShareEntry = "User with username {Username} change permission of entry with Id {EntryId} to User with id {SharedUsername}"; public const string UpdateEntry = "User with username {Username} update entry with Id {EntryId}"; public const string UploadDigitalFile = "User with username {Username} upload an entry with id {EntryId}"; - public const string UploadSharedEntry = "User with username {Username} upload an emtry with id {EntryId}"; + public const string UploadSharedEntry = "User with username {Username} upload an entry with id {EntryId}"; } \ No newline at end of file diff --git a/src/Application/Common/Models/HtmlMailData.cs b/src/Application/Common/Models/HtmlMailData.cs index 105953a9..238b3846 100644 --- a/src/Application/Common/Models/HtmlMailData.cs +++ b/src/Application/Common/Models/HtmlMailData.cs @@ -32,8 +32,8 @@ public class ResetPasswordTemplateVariables { [JsonPropertyName("user_email")] public string UserEmail { get; set; } - [JsonPropertyName("token_hash")] - public string TokenHash { get; set; } + [JsonPropertyName("token")] + public string Token { get; set; } [JsonPropertyName("user_password")] public string UserPassword { get; set; } } diff --git a/src/Application/Entries/Commands/CreateEntry.cs b/src/Application/Entries/Commands/CreateEntry.cs index 86647ee2..0b6f4ae7 100644 --- a/src/Application/Entries/Commands/CreateEntry.cs +++ b/src/Application/Entries/Commands/CreateEntry.cs @@ -95,7 +95,7 @@ public async Task Handle(Command request, CancellationToken cancellati var entryEntity = new Entry() { - Name = request.Name.Trim(), + Name = request.IsDirectory ? request.Name.Trim() : request.Name.Trim()[0..request.Name.Trim().LastIndexOf(".", StringComparison.Ordinal)], Path = request.Path.Trim(), CreatedBy = request.CurrentUser.Id, Uploader = request.CurrentUser, @@ -121,7 +121,7 @@ public async Task Handle(Command request, CancellationToken cancellati else { var entries = _context.Entries.AsQueryable() - .Where(x => x.Name.Trim().Substring(0, request.Name.Trim().Length).Equals(request.Name.Trim()) + .Where(x => x.Name.Trim().Substring(0, entryEntity.Name.Trim().Length).Equals(entryEntity.Name.Trim()) && x.Path.Trim().Equals(request.Path.Trim()) && x.FileId != null && x.OwnerId == request.CurrentUser.Id); @@ -137,14 +137,7 @@ public async Task Handle(Command request, CancellationToken cancellati while (true) { var temp = ""; - if (i == 0) - { - temp = entryEntity.Name; - } - else - { - temp = $"{entryEntity.Name} ({i})"; - } + temp = i == 0 ? entryEntity.Name : $"{entryEntity.Name} ({i})"; var checkEntry = await entries.AnyAsync(x => x.Name.Equals(temp),cancellationToken); if (!checkEntry) diff --git a/src/Application/Entries/Commands/CreateSharedEntry.cs b/src/Application/Entries/Commands/CreateSharedEntry.cs index ebefb8b7..6cfcb6af 100644 --- a/src/Application/Entries/Commands/CreateSharedEntry.cs +++ b/src/Application/Entries/Commands/CreateSharedEntry.cs @@ -75,11 +75,11 @@ public async Task Handle(Command request, CancellationToken cancellati } var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - var entryPath = permission.Entry.Path.Equals("/") ? permission.Entry.Path + permission.Entry.Name : $"{permission.Entry.Path}/{request.Name.Trim()}"; + var entryPath = permission.Entry.Path.Equals("/") ? permission.Entry.Path + permission.Entry.Name : $"{permission.Entry.Path}/{permission.Entry.Name}"; var entity = new Entry() { - Name = request.Name.Trim(), + Name = request.IsDirectory ? request.Name.Trim() : request.Name.Trim()[0..request.Name.Trim().LastIndexOf(".", StringComparison.Ordinal)], Path = entryPath, CreatedBy = request.CurrentUser.Id, Uploader = request.CurrentUser, @@ -108,6 +108,31 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("File size must be lower than 20MB"); } + var entries = _context.Entries + .Where(x => x.Name.Trim().Substring(0, entity.Name.Trim().Length).Equals(entity.Name.Trim()) + && x.Path.Trim().Equals(entryPath) + && x.FileId != null + && x.OwnerId == permission.Entry.OwnerId); + + if (entries.Any()) + { + var i = 0; + while (true) + { + var temp = ""; + temp = i == 0 ? entity.Name : $"{entity.Name} ({i})"; + var checkEntry = await entries.AnyAsync(x => x.Name.Equals(temp),cancellationToken); + + if (!checkEntry) + { + entity.Name = temp; + break; + } + + i++; + } + } + var fileEntity = new FileEntity() { FileData = request.FileData.ToArray(), @@ -121,7 +146,6 @@ public async Task Handle(Command request, CancellationToken cancellati entity.File = fileEntity; } - var result = await _context.Entries.AddAsync(entity, cancellationToken); var permissionEntity = new EntryPermission() @@ -129,7 +153,6 @@ public async Task Handle(Command request, CancellationToken cancellati EntryId = result.Entity.Id, Entry = result.Entity, EmployeeId = request.CurrentUser.Id, - Employee = request.CurrentUser, ExpiryDateTime = null, AllowedOperations = $"{EntryOperation.View.ToString()},{EntryOperation.Edit.ToString()}" }; diff --git a/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs index 1f0f9fd0..25f80f1e 100644 --- a/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs +++ b/src/Application/Entries/Queries/GetAllSharedEntriesPaginated.cs @@ -41,6 +41,8 @@ public async Task> Handle(Query request, CancellationTok .ThenInclude(y => y.Uploader) .Include(x => x.Entry) .ThenInclude(y => y.Owner) + .Include(x => x.Entry) + .ThenInclude(y => y.File) .Where(x => x.EmployeeId == request.CurrentUser.Id && x.AllowedOperations diff --git a/src/Application/Entries/Queries/GetSharedEntryById.cs b/src/Application/Entries/Queries/GetSharedEntryById.cs index 33e0fc17..e515843a 100644 --- a/src/Application/Entries/Queries/GetSharedEntryById.cs +++ b/src/Application/Entries/Queries/GetSharedEntryById.cs @@ -35,6 +35,8 @@ public async Task Handle(Query request, CancellationToken cancellation .ThenInclude(x => x.Uploader) .Include(x => x.Entry) .ThenInclude(x => x.Owner) + .Include(x => x.Entry) + .ThenInclude(x => x.File) .FirstOrDefaultAsync(x => x.EntryId == request.EntryId && x.EmployeeId == request.CurrentUser.Id, cancellationToken); diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs index c0aed008..41b48e34 100644 --- a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -35,6 +35,6 @@ public async Task Handle(UserCreatedEvent notification, CancellationToken cancel await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken, cancellationToken); await _authDbContext.SaveChangesAsync(cancellationToken); - _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, resetPasswordToken.TokenHash); + _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, token); } } \ No newline at end of file diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index dc045777..ca280627 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -17,7 +17,7 @@ public MailService(IOptions mailSettingsOptions) _mailSettings = mailSettingsOptions.Value; } - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string token) { var data = new HtmlMailData() { @@ -35,7 +35,7 @@ public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword { UserEmail = userEmail, UserPassword = temporaryPassword, - TokenHash = tokenHash + Token = token }, }; diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs index 3327efea..587cf774 100644 --- a/tests/Application.Tests.Integration/CustomMailService.cs +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -4,7 +4,7 @@ namespace Application.Tests.Integration; public class CustomMailService : IMailService { - public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string tokenHash) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string token) { return true; } From 048a2afda005ca4621c738302f90d610fd94b941 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Mon, 31 Jul 2023 01:18:07 +0700 Subject: [PATCH 161/162] fix: backgrond workers --- src/Api/Controllers/DashboardController.cs | 16 ---------------- src/Api/Services/BackgroundWorkers.cs | 12 ------------ .../Authorization/RequiresRoleAttribute.cs | 2 -- 3 files changed, 30 deletions(-) diff --git a/src/Api/Controllers/DashboardController.cs b/src/Api/Controllers/DashboardController.cs index cc1c7428..346c786f 100644 --- a/src/Api/Controllers/DashboardController.cs +++ b/src/Api/Controllers/DashboardController.cs @@ -11,13 +11,6 @@ namespace Api.Controllers; public class DashboardController : ApiControllerBase { - private readonly ICurrentUserService _currentUserService; - - public DashboardController(ICurrentUserService currentUserService) - { - _currentUserService = currentUserService; - } - [RequiresRole(IdentityData.Roles.Admin)] [HttpPost("import-documents")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -66,13 +59,4 @@ public async Task>> GetUserWithLargestDrive var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("online-users")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task> GetOnlineUsers() - { - return Ok(Result.Succeed(RequiresRoleAttribute.OnlineUsers.Count)); - } } \ No newline at end of file diff --git a/src/Api/Services/BackgroundWorkers.cs b/src/Api/Services/BackgroundWorkers.cs index 0068d066..70c1228f 100644 --- a/src/Api/Services/BackgroundWorkers.cs +++ b/src/Api/Services/BackgroundWorkers.cs @@ -24,7 +24,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) DisposeExpiredEntries(TimeSpan.FromSeconds(10), stoppingToken), DisposeExpiredPermissions(TimeSpan.FromSeconds(10), stoppingToken), HandleOverdueRequest(TimeSpan.FromMinutes(2), stoppingToken), - HandleOnlineUsers(TimeSpan.FromSeconds(10), stoppingToken), }; await Task.WhenAll(workers.ToArray()); @@ -76,15 +75,4 @@ private async Task HandleOverdueRequest(TimeSpan delay, CancellationToken stoppi await context.SaveChangesAsync(stoppingToken); await Task.Delay(delay, stoppingToken); } - - private async Task HandleOnlineUsers(TimeSpan delay, CancellationToken stoppingToken) - { - var onlineUsers = RequiresRoleAttribute.OnlineUsers; - foreach (var id in onlineUsers.Keys - .Where(id => DateTime.Now - onlineUsers[id] < delay)) - { - RequiresRoleAttribute.OnlineUsers.Remove(id); - } - await Task.Delay(delay, stoppingToken); - } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs index a1c63d21..79239f56 100644 --- a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs +++ b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs @@ -11,7 +11,6 @@ namespace Infrastructure.Identity.Authorization; public class RequiresRoleAttribute : Attribute, IAuthorizationFilter { private readonly string[] _claimValues; - public static Dictionary OnlineUsers { get; } = new(); public RequiresRoleAttribute(params string[] claimValues) { @@ -34,7 +33,6 @@ public void OnAuthorization(AuthorizationFilterContext context) if (_claimValues.Any(claimValue => user!.Role.Equals(claimValue))) { - OnlineUsers[user.Id] = DateTime.Now; return; } From 0ae767c4f3d2faad604caa80ee9659af3cf6fc3a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Mon, 31 Jul 2023 01:26:46 +0700 Subject: [PATCH 162/162] update: seed users have default password --- src/Infrastructure/Persistence/ApplicationDbContextSeed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index 5bb6d6a5..1682d2cb 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -375,7 +375,7 @@ private static User CreateRandomEmployee(string username, string pepper) Email = new Faker().Person.Email, FirstName = new Faker().Person.FirstName, LastName = new Faker().Person.LastName, - PasswordHash = new Faker().Random.String().HashPasswordWith(salt, pepper), + PasswordHash = "employee".HashPasswordWith(salt, pepper), PasswordSalt = salt, IsActive = true, IsActivated = true,