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}}"
diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml
new file mode 100644
index 00000000..2928e706
--- /dev/null
+++ b/.github/workflows/versioning.yml
@@ -0,0 +1,21 @@
+name: Bump version
+on:
+ push:
+ branches:
+ - 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 }}
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/docker-compose.test.yml b/docker-compose.test.yml
index d7d44b92..a1e8f565 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -8,8 +8,8 @@ services:
ports:
- '8888:80'
environment:
- - ASPNETCORE_ENVIRONMENT=Development
- - PROFILE_DatabaseSettings__ConnectionString=Server=database;Port=5432;Database=mytestdb;User ID=profiletester;Password=supasupasecured;
+ - ASPNETCORE_ENVIRONMENT=Testing
+ - 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/docker-compose.yml b/docker-compose.yml
index f478e29b..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
@@ -20,4 +29,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
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index 09720f26..d45ec98c 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -4,6 +4,7 @@
net6.0
enable
enable
+ true
@@ -13,6 +14,8 @@
+
+
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 9420dab1..390e032b 100644
--- a/src/Api/ConfigureServices.cs
+++ b/src/Api/ConfigureServices.cs
@@ -1,15 +1,24 @@
+using System.Reflection;
+using Api.Common;
using Api.Middlewares;
using Api.Policies;
+using Api.Services;
+using Application.Common.Interfaces;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
+using Microsoft.OpenApi.Models;
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();
services.AddControllers(opt =>
opt.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer())));
@@ -18,11 +27,20 @@ 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.AllowAnyOrigin();
+ builder.WithOrigins(frontendBaseUrl);
builder.AllowAnyHeader();
builder.AllowAnyMethod();
+ builder.AllowCredentials();
});
});
@@ -30,7 +48,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;
}
@@ -39,6 +68,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/AuthController.cs b/src/Api/Controllers/AuthController.cs
index b0941458..c4e980c8 100644
--- a/src/Api/Controllers/AuthController.cs
+++ b/src/Api/Controllers/AuthController.cs
@@ -1,9 +1,13 @@
using System.IdentityModel.Tokens.Jwt;
-using Api.Controllers.Payload.Requests;
+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;
using Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -16,92 +20,152 @@ 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;
}
+ ///
+ /// Login
+ ///
+ /// Login credentials
+ /// A LoginResult indicating the result of logging in
[AllowAnonymous]
[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);
- 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);
- [Authorize]
+ 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,
+ };
+
+ 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 =>
+ {
+ var r = new Result
+ {
+ Data = new NotActivatedLoginResult()
+ {
+ Token = token,
+ }
+ };
+ return Unauthorized(r);
+ });
+
+ }
+
+ ///
+ /// Refresh session and token
+ ///
+ /// An IActionResult indicating the result of refreshing token
[HttpPost]
- public async Task Logout()
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public async Task Refresh()
{
var refreshToken = Request.Cookies[nameof(RefreshToken)];
var jweToken = Request.Cookies["JweToken"];
- var loggedOut = await _identityService.LogoutAsync(jweToken!, refreshToken!);
-
- if (!loggedOut) return Ok();
-
- RemoveJweToken();
- RemoveRefreshToken();
+ try
+ {
+ var authResult = await _identityService.RefreshTokenAsync(jweToken!, refreshToken!);
+
+ SetRefreshToken(authResult.RefreshToken);
+ SetJweToken(authResult.Token, authResult.RefreshToken);
+ }
+ catch (AuthenticationException)
+ {
+ RemoveJweToken();
+ RemoveRefreshToken();
+ throw;
+ }
return Ok();
}
-
+
+ ///
+ /// Validate current user
+ ///
+ /// An IActionResult indicating the result of validating the user
[Authorize]
[HttpPost]
- public async Task Refresh()
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ 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"];
- var authResult = await _identityService.RefreshTokenAsync(jweToken!, refreshToken!);
-
- SetRefreshToken(authResult.RefreshToken);
- SetJweToken(authResult.Token);
+ RemoveJweToken();
+ RemoveRefreshToken();
+ await _identityService.LogoutAsync(jweToken!, refreshToken!);
+
return Ok();
}
- [Authorize]
[HttpPost]
- public async Task Validate()
+ public async Task ResetPassword([FromBody] ResetPasswordRequest request)
{
- var refreshToken = Request.Cookies[nameof(RefreshToken)];
- var jweToken = Request.Cookies["JweToken"];
+ if (string.IsNullOrEmpty(request.NewPassword))
+ {
+ return BadRequest("Password cannot be empty.");
+ }
- var validated = await _identityService.Validate(jweToken!, refreshToken!);
-
- if (validated)
+ if (!request.NewPassword.Equals(request.ConfirmPassword))
{
- return Ok();
+ return BadRequest("Confirm password must match with new password.");
}
- return Unauthorized();
+ await _identityService.ResetPassword(request.Token, request.NewPassword);
+ return Ok();
}
-
- 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);
diff --git a/src/Api/Controllers/BinController.cs b/src/Api/Controllers/BinController.cs
new file mode 100644
index 00000000..6b987051
--- /dev/null
+++ b/src/Api/Controllers/BinController.cs
@@ -0,0 +1,138 @@
+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;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+
+public class BinController : ApiControllerBase
+{
+ private readonly ICurrentUserService _currentUserService;
+
+ public BinController(ICurrentUserService currentUserService)
+ {
+ _currentUserService = currentUserService;
+ }
+
+ ///
+ /// Move an entry to bin
+ ///
+ ///
+ /// an EntryDto
+ [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));
+ }
+
+ ///
+ /// Restore an entry from bin
+ ///
+ ///
+ /// an EntryDto
+ [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));
+ }
+
+
+ ///
+ /// Remove an entry from bin
+ ///
+ ///
+ /// an EntryDto
+ [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));
+ }
+
+
+ ///
+ /// Get entry in the bin by Id
+ ///
+ ///
+ /// an EntryDto
+ [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));
+ }
+
+ ///
+ /// Get all entry in the bin
+ ///
+ ///
+ /// a paginated list of EntryDto
+ [RequiresRole(IdentityData.Roles.Employee)]
+ [HttpGet("entries")]
+ public async Task>>> GetAllBinEntriesPaginated(
+ [FromQuery] GetAllBinEntriesPaginatedQueryParameters queryParameters)
+ {
+ var currentUser = _currentUserService.GetCurrentUser();
+
+ var command = new GetAllBinEntriesPaginated.Query()
+ {
+ CurrentUser = currentUser,
+ EntryPath = queryParameters.EntryPath,
+ 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));
+ }
+}
diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs
new file mode 100644
index 00000000..43379604
--- /dev/null
+++ b/src/Api/Controllers/BorrowsController.cs
@@ -0,0 +1,297 @@
+using Api.Controllers.Payload.Requests;
+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;
+using Application.Common.Models.Dtos.Physical;
+using Application.Identity;
+using Application.Loggings.Queries;
+using Infrastructure.Identity.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers;
+
+[Route("api/v1/documents/[controller]")]
+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,
+ BorrowReason = request.Reason,
+ };
+ var result = await Mediator.Send(command);
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Get a borrow request by id
+ ///
+ /// A BorrowDto of the retrieved borrow
+ [RequiresRole(IdentityData.Roles.Admin ,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.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task>>> GetAllRequestsPaginated(
+ [FromQuery] GetAllBorrowRequestsPaginatedQueryParameters queryParameters)
+ {
+ var currentUser = _currentUserService.GetCurrentUser();
+ var command = new GetAllBorrowRequestsPaginated.Query()
+ {
+ CurrentUser = currentUser,
+ RoomId = queryParameters.RoomId,
+ EmployeeId = queryParameters.EmployeeId,
+ DocumentId = queryParameters.DocumentId,
+ Statuses = queryParameters.Statuses,
+ Page = queryParameters.Page,
+ Size = queryParameters.Size,
+ SortBy = queryParameters.SortBy,
+ SortOrder = queryParameters.SortOrder,
+ };
+ var result = await Mediator.Send(command);
+ return Ok(Result>.Succeed(result));
+ }
+
+ ///
+ /// 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)]
+ [HttpPut("staffs/{borrowId:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task>> ApproveOrRejectRequest(
+ [FromRoute] Guid borrowId,
+ [FromBody] ApproveOrRejectBorrowRequestRequest request)
+ {
+ 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));
+ }
+
+ ///
+ /// 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 currentStaff = _currentUserService.GetCurrentUser();
+ var command = new CheckoutDocument.Command()
+ {
+ CurrentStaff = currentStaff,
+ 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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new ReturnDocument.Command()
+ {
+ CurrentUser = currentUser,
+ 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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new UpdateBorrow.Command()
+ {
+ CurrentUser = currentUser,
+ BorrowId = borrowId,
+ BorrowFrom = request.BorrowFrom,
+ BorrowTo = request.BorrowTo,
+ BorrowReason = request.Reason,
+ };
+ 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 currentUserId = _currentUserService.GetId();
+ var command = new CancelBorrowRequest.Command()
+ {
+ CurrentUserId = currentUserId,
+ BorrowId = borrowId,
+ };
+ 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));
+ }
+
+ ///
+ /// 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.
+ ///
+ /// 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/DashboardController.cs b/src/Api/Controllers/DashboardController.cs
new file mode 100644
index 00000000..346c786f
--- /dev/null
+++ b/src/Api/Controllers/DashboardController.cs
@@ -0,0 +1,62 @@
+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;
+
+namespace Api.Controllers;
+
+public class DashboardController : ApiControllerBase
+{
+ [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));
+ }
+}
\ No newline at end of file
diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs
index 6a010c00..34ea8fd6 100644
--- a/src/Api/Controllers/DepartmentsController.cs
+++ b/src/Api/Controllers/DepartmentsController.cs
@@ -1,7 +1,12 @@
-using Application.Common.Models;
-using Application.Departments.Commands.CreateDepartment;
-using Application.Departments.Queries.GetAllDepartments;
+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;
@@ -10,32 +15,118 @@ namespace Api.Controllers;
public class DepartmentsController : ApiControllerBase
{
+ private readonly ICurrentUserService _currentUserService;
+
+ public DepartmentsController(ICurrentUserService currentUserService)
+ {
+ _currentUserService = currentUserService;
+ }
+
+ ///
+ /// Get a department by it 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)
+ {
+ var role = _currentUserService.GetRole();
+ var userDepartmentId = _currentUserService.GetDepartmentId();
+ var query = new GetDepartmentById.Query()
+ {
+ UserRole = role,
+ UserDepartmentId = userDepartmentId,
+ DepartmentId = departmentId,
+ };
+ var result = await Mediator.Send(query);
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Get rooms based on department 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 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));
+ }
+
///
- /// Create a department
+ /// Get all departments
///
- /// command parameter to create a department
- /// Result[DepartmentDto]
+ /// A list of DepartmentDto
+ [RequiresRole(IdentityData.Roles.Admin)]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task>>> GetAll()
+ {
+ var result = await Mediator.Send(new GetAllDepartments.Query());
+ return Ok(Result>.Succeed(result));
+ }
+
+ ///
+ /// Add a department
+ ///
+ /// Add department details
+ /// A DepartmentDto of the the added department
[RequiresRole(IdentityData.Roles.Admin)]
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
- public async Task>> CreateDepartment([FromBody] CreateDepartmentCommand command)
+ public async Task>> Add(
+ [FromBody] AddDepartmentRequest request)
{
+ var command = new AddDepartment.Command()
+ {
+ Name = request.Name,
+ };
var result = await Mediator.Send(command);
return Ok(Result.Succeed(result));
}
///
- /// Get all documents
+ /// Delete a department
///
- /// a Result of an IEnumerable of DepartmentDto
- [HttpGet]
+ /// 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)]
- public async Task>>> GetAllDepartments()
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task>> Delete([FromRoute] Guid departmentId)
{
- var result = await Mediator.Send(new GetAllDepartmentsQuery());
- return Ok(Result>.Succeed(result));
+ var command = new DeleteDepartment.Command()
+ {
+ 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 7d491b2b..b810cacc 100644
--- a/src/Api/Controllers/DocumentsController.cs
+++ b/src/Api/Controllers/DocumentsController.cs
@@ -1,12 +1,12 @@
+using Api.Controllers.Payload.Requests.Documents;
+using Api.Controllers.Payload.Requests.Users;
+using Application.Common.Extensions;
+using Application.Common.Interfaces;
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.Documents.Commands;
+using Application.Documents.Queries;
using Application.Identity;
-using Application.Users.Queries;
using Infrastructure.Identity.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -14,60 +14,354 @@ namespace Api.Controllers;
public class DocumentsController : ApiControllerBase
{
- [HttpPost]
+ 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.Employee)]
+ [HttpGet("{documentId:guid}")]
[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)
+ public async Task>> GetById(
+ [FromRoute] Guid documentId)
{
- var result = await Mediator.Send(command);
+ var currentUser = _currentUserService.GetCurrentUser();
+ var query = new GetDocumentById.Query()
+ {
+ CurrentUser = currentUser,
+ DocumentId = documentId,
+ };
+ 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.Admin, IdentityData.Roles.Staff)]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ 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,
+ SearchTerm = queryParameters.SearchTerm,
+ Page = queryParameters.Page,
+ 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
+ ///
+ /// A list of document types
+ [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)]
[HttpGet("types")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
-
public async Task>>> GetAllDocumentTypes()
{
- var result = await Mediator.Send(new GetAllDocumentTypesQuery());
+ 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.Staff)]
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ 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,
+ FolderId = request.FolderId,
+ ImporterId = request.ImporterId,
+ IsPrivate = request.IsPrivate,
+ };
+ var result = await Mediator.Send(command);
+ return Ok(Result.Succeed(result));
+ }
- [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)]
- [HttpGet]
+ ///
+ /// Update a document
+ ///
+ /// Id of the document to be updated
+ /// Update document details
+ /// A DocumentDto of the updated document
+ [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>>> GetAllDocuments(Guid? roomId, Guid? lockerId, Guid? folderId, int? page, int? size, string? sortBy, string? sortOrder)
+ public async Task>> Update(
+ [FromRoute] Guid documentId,
+ [FromBody] UpdateDocumentRequest request)
{
- var query = new GetAllDocumentsPaginatedQuery()
+ var currentUser = _currentUserService.GetCurrentUser();
+ if (currentUser.Department is null)
+ {
+ return Forbid();
+ }
+ var query = new UpdateDocument.Command()
{
- RoomId = roomId,
- LockerId = lockerId,
- FolderId = folderId,
- Page = page,
- Size = size,
- SortBy = sortBy,
- SortOrder = sortOrder
+ CurrentUser = currentUser,
+ DocumentId = documentId,
+ Title = request.Title,
+ Description = request.Description,
+ DocumentType = request.DocumentType,
+ IsPrivate = request.IsPrivate,
};
var result = await Mediator.Send(query);
- return Ok(Result>.Succeed(result));
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Delete a document
+ ///
+ /// Id of the document to be deleted
+ /// 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)
+ {
+ 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));
+ }
+
- [HttpGet("{id:guid}")]
- public async Task>> GetDocumentById(Guid id)
+ ///
+ /// 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
+ ///
+ /// 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 query = new GetDocumentByIdQuery()
+ var currentUser = _currentUserService.GetCurrentUser();
+ var query = new DownloadDocumentFile.Query()
{
- Id = id
+ 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);
+ }
+
+ ///
+ /// 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));
}
-}
\ No newline at end of file
+}
diff --git a/src/Api/Controllers/EntriesController.cs b/src/Api/Controllers/EntriesController.cs
new file mode 100644
index 00000000..064e4c89
--- /dev/null
+++ b/src/Api/Controllers/EntriesController.cs
@@ -0,0 +1,215 @@
+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.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.Entries.GetAllEntriesPaginatedQueryParameters;
+
+namespace Api.Controllers;
+
+public class EntriesController : ApiControllerBase
+{
+ private readonly ICurrentUserService _currentUserService;
+
+ public EntriesController(ICurrentUserService currentUserService)
+ {
+ _currentUserService = currentUserService;
+ }
+
+ ///
+ /// Upload a file or create a directory
+ ///
+ ///
+ /// an EntryDto
+ [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)]
+ [HttpPost]
+ public async Task>> UploadEntry(
+ [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.")
+ });
+ }
+
+ CreateEntry.Command command;
+
+ if (request.IsDirectory)
+ {
+ command = new CreateEntry.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 CreateEntry.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));
+ }
+
+ ///
+ /// Update an entry
+ ///
+ ///
+ ///
+ /// an EntryDto
+ [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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new UpdateEntry.Command()
+ {
+ Name = request.Name,
+ EntryId = entryId,
+ CurrentUser = currentUser
+ };
+
+ var result = await Mediator.Send(command);
+ return Ok(Result.Succeed(result));
+ }
+
+
+ ///
+ /// Get all entries paginated
+ ///
+ /// a paginated list of EntryDto
+ [RequiresRole(IdentityData.Roles.Employee)]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ 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,
+ SortBy = queryParameters.SortBy,
+ SortOrder = queryParameters.SortOrder
+ };
+
+ 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)]
+ [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));
+ }
+
+ ///
+ /// Share an entry
+ ///
+ ///
+ /// an EntryPermissionDto
+ [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,
+ CanEdit = request.CanEdit,
+ };
+ 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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new DownloadDigitalFile.Command()
+ {
+ CurrentUser = currentUser,
+ 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/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs
index 77664c44..4ec4d2e5 100644
--- a/src/Api/Controllers/FoldersController.cs
+++ b/src/Api/Controllers/FoldersController.cs
@@ -1,7 +1,10 @@
+using Api.Controllers.Payload.Requests.Folders;
+using Application.Common.Extensions;
+using Application.Common.Interfaces;
using Application.Common.Models;
using Application.Common.Models.Dtos.Physical;
-using Application.Folders.Commands.AddFolder;
-using Application.Folders.Commands.DisableFolder;
+using Application.Folders.Commands;
+using Application.Folders.Queries;
using Application.Identity;
using Infrastructure.Identity.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -10,22 +13,154 @@ namespace Api.Controllers;
public class FoldersController : ApiControllerBase
{
- [RequiresRole(IdentityData.Roles.Staff)]
+ private readonly ICurrentUserService _currentUserService;
+
+ public FoldersController(ICurrentUserService currentUserService)
+ {
+ _currentUserService = currentUserService;
+ }
+
+ ///
+ /// 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 currentUserRole = _currentUserService.GetRole();
+ var staffRoomId = _currentUserService.GetCurrentRoomForStaff();
+ var query = new GetFolderById.Query()
+ {
+ CurrentUserRole = currentUserRole,
+ CurrentStaffRoomId = staffRoomId,
+ FolderId = folderId,
+ };
+ var result = await Mediator.Send(query);
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Get all folders paginated
+ ///
+ /// Get all folders paginated 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 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,
+ 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, IdentityData.Roles.Staff)]
[HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [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,
+ LockerId = request.LockerId,
+ };
+ 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, IdentityData.Roles.Staff)]
+ [HttpDelete("{folderId:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
- public async Task>> AddFolder([FromBody] AddFolderCommand command)
+ public async Task>> RemoveFolder([FromRoute] Guid folderId)
{
+ var currentUser = _currentUserService.GetCurrentUser();
+ Guid? staffRoomId = null;
+ if (currentUser.Role.IsStaff())
+ {
+ 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));
}
- [HttpPut("disable")]
- public async Task>> DisableFolder([FromBody] DisableFolderCommand command)
+ ///
+ /// Update a folder
+ ///
+ /// 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)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ 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,
+ Capacity = request.Capacity,
+ IsAvailable = request.IsAvailable,
+ };
var result = await Mediator.Send(command);
return Ok(Result.Succeed(result));
}
diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs
new file mode 100644
index 00000000..d2875e22
--- /dev/null
+++ b/src/Api/Controllers/ImportRequestsController.cs
@@ -0,0 +1,188 @@
+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,
+ Statuses = queryParameters.Statuses,
+ 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 1dd1b3ce..58d1c502 100644
--- a/src/Api/Controllers/LockersController.cs
+++ b/src/Api/Controllers/LockersController.cs
@@ -1,9 +1,10 @@
-using Application.Common.Models;
+using Api.Controllers.Payload.Requests.Lockers;
+using Application.Common.Interfaces;
+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;
+using Application.Lockers.Queries;
using Infrastructure.Identity.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -11,28 +12,146 @@ namespace Api.Controllers;
public class LockersController : ApiControllerBase
{
- [RequiresRole(IdentityData.Roles.Staff)]
+ 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)
+ {
+ 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);
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Get all lockers paginated
+ ///
+ /// 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 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,
+ 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));
+ }
+
+ ///
+ /// 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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new AddLocker.Command()
+ {
+ CurrentUser = currentUser,
+ Name = request.Name,
+ Description = request.Description,
+ Capacity = request.Capacity,
+ RoomId = request.RoomId,
+ };
var result = await Mediator.Send(command);
return Ok(Result.Succeed(result));
}
- [HttpPut("disable")]
- public async Task>> DisableLocker([FromBody] DisableLockerCommand command)
+ ///
+ /// Remove a locker
+ ///
+ /// 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)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [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);
return Ok(Result.Succeed(result));
}
- [HttpPut("enable")]
- public async Task>> EnableLocker([FromBody] EnableLockerCommand command)
+ ///
+ /// Update a 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("{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 currentUser = _currentUserService.GetCurrentUser();
+ var command = new UpdateLocker.Command()
+ {
+ CurrentUser = currentUser,
+ LockerId = lockerId,
+ 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/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/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/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/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs
new file mode 100644
index 00000000..19f038fd
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/BinEntries/GetAllBinEntriesPaginatedQueryParameters.cs
@@ -0,0 +1,7 @@
+namespace Api.Controllers.Payload.Requests.BinEntries;
+
+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/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/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/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..ca2c22b7
--- /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
+{
+ public string? Status { get; set; }
+}
\ No newline at end of file
diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs
new file mode 100644
index 00000000..7fc42536
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs
@@ -0,0 +1,15 @@
+namespace Api.Controllers.Payload.Requests.Borrows;
+
+///
+/// Query parameters for getting all borrow requests with pagination as admin
+///
+public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParameters
+{
+ ///
+ /// Id of the room to get borrow requests in
+ ///
+ 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/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/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/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/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/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..88b9ff9e
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs
@@ -0,0 +1,8 @@
+namespace Api.Controllers.Payload.Requests.Documents;
+
+public class GetAllDocumentsForEmployeePaginatedQueryParameters : PaginatedQueryParameters
+{
+ 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
new file mode 100644
index 00000000..94cca9b8
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs
@@ -0,0 +1,28 @@
+namespace Api.Controllers.Payload.Requests.Documents;
+
+///
+/// Query parameters for getting all documents with pagination
+///
+public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters
+{
+ public Guid? UserId { get; set; }
+ ///
+ /// 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; }
+ 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/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs
new file mode 100644
index 00000000..49cea0fe
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs
@@ -0,0 +1,32 @@
+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; }
+ ///
+ ///
+ ///
+ public bool IsPrivate { 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
new file mode 100644
index 00000000..97626d8a
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs
@@ -0,0 +1,21 @@
+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!;
+ public bool IsPrivate { 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
new file mode 100644
index 00000000..462cbbc0
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Entries/GetAllEntriesPaginatedQueryParameters.cs
@@ -0,0 +1,9 @@
+namespace Api.Controllers.Payload.Requests.Entries;
+
+///
+/// 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/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/Entries/ShareEntryPermissionRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs
new file mode 100644
index 00000000..98b64b2f
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Entries/ShareEntryPermissionRequest.cs
@@ -0,0 +1,9 @@
+namespace Api.Controllers.Payload.Requests.Entries;
+
+public class ShareEntryPermissionRequest
+{
+ public Guid UserId { get; set; }
+ public DateTime? ExpiryDate { get; set; }
+ public bool CanView { get; set; }
+ public bool CanEdit { get; set; }
+}
\ No newline at end of file
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/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs
new file mode 100644
index 00000000..ff4ddbf9
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Entries/UploadDigitalFileRequest.cs
@@ -0,0 +1,12 @@
+namespace Api.Controllers.Payload.Requests.Entries;
+
+///
+///
+///
+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/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs b/src/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs
new file mode 100644
index 00000000..21363822
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Entries/UploadSharedEntryRequest.cs
@@ -0,0 +1,11 @@
+namespace Api.Controllers.Payload.Requests.Entries;
+
+///
+///
+///
+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/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
new file mode 100644
index 00000000..298a30b0
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Folders/GetAllFoldersPaginatedQueryParameters.cs
@@ -0,0 +1,20 @@
+namespace Api.Controllers.Payload.Requests.Folders;
+
+///
+/// Query parameters for getting all folders with pagination
+///
+public class GetAllFoldersPaginatedQueryParameters : PaginatedQueryParameters
+{
+ ///
+ /// Search term
+ ///
+ public string? SearchTerm { get; set; }
+ ///
+ /// 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; }
+}
\ 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..b62943b2
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Folders/UpdateFolderRequest.cs
@@ -0,0 +1,24 @@
+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; }
+ ///
+ /// 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/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs
new file mode 100644
index 00000000..68917040
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs
@@ -0,0 +1,28 @@
+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; }
+ ///
+ /// 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/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs
new file mode 100644
index 00000000..dfb22205
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs
@@ -0,0 +1,11 @@
+namespace Api.Controllers.Payload.Requests.ImportRequests;
+
+///
+///
+///
+public class GetAllImportRequestsPaginatedQueryParameters : PaginatedQueryParameters
+{
+ 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/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs
new file mode 100644
index 00000000..21374f9a
--- /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; set; } = null!;
+ ///
+ /// Description of the locker to be updated
+ ///
+ public string? Description { get; set; }
+ ///
+ /// Id of the room that this locker will be in
+ ///
+ public Guid RoomId { get; set; }
+ ///
+ /// Number of folders this locker can hold
+ ///
+ 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..25372261
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Lockers/GetAllLockersPaginatedQueryParameters.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Api.Controllers.Payload.Requests.Lockers;
+
+///
+/// Query parameters for getting all lockers with pagination
+///
+public class GetAllLockersPaginatedQueryParameters : PaginatedQueryParameters
+{
+ ///
+ /// Id of the room to find lockers in
+ ///
+ public Guid? RoomId { get; set; }
+ ///
+ /// Search term
+ ///
+ public string? SearchTerm { 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..a6daa0aa
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Lockers/UpdateLockerRequest.cs
@@ -0,0 +1,24 @@
+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; }
+ ///
+ /// Status of locker to be updated
+ ///
+ public bool IsAvailable { 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/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/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..83ff67a1
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs
@@ -0,0 +1,14 @@
+namespace Api.Controllers.Payload.Requests.Rooms;
+
+///
+/// Query parameters for getting all rooms with pagination
+///
+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/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..d277d097
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs
@@ -0,0 +1,24 @@
+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; }
+ ///
+ /// 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
new file mode 100644
index 00000000..6e33caa6
--- /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 StaffId { 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
new file mode 100644
index 00000000..323ec479
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Staffs/GetAllStaffsPaginatedQueryParameters.cs
@@ -0,0 +1,12 @@
+namespace Api.Controllers.Payload.Requests.Staffs;
+
+///
+/// Query parameters for getting all staffs with pagination
+///
+public class GetAllStaffsPaginatedQueryParameters : PaginatedQueryParameters
+{
+ ///
+ /// Search term
+ ///
+ public string? SearchTerm { 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..ad129e16
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Users/AddUserRequest.cs
@@ -0,0 +1,36 @@
+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!;
+ ///
+ /// 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/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/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs
new file mode 100644
index 00000000..bdb7a102
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Users/GetAllSharedUsersOfASharedEntryPaginatedQueryParameters.cs
@@ -0,0 +1,17 @@
+namespace Api.Controllers.Payload.Requests.Users;
+
+public class GetAllSharedUsersPaginatedQueryParameters
+{
+ ///
+ /// 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/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs
new file mode 100644
index 00000000..738a1017
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs
@@ -0,0 +1,17 @@
+namespace Api.Controllers.Payload.Requests.Users;
+
+///
+/// Query parameters for getting all users with pagination
+///
+public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters
+{
+ ///
+ /// Id of the department to find users in
+ ///
+ public Guid[]? DepartmentIds { get; set; }
+ public string? Role { get; set; }
+ ///
+ /// Search term
+ ///
+ public string? SearchTerm { get; set; }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..959567d4
--- /dev/null
+++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs
@@ -0,0 +1,22 @@
+namespace Api.Controllers.Payload.Requests.Users;
+
+///
+/// Request details to update a user
+///
+public class UpdateUserRequest
+{
+ ///
+ /// 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 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/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/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/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs
index 8d8916d6..a285a3fd 100644
--- a/src/Api/Controllers/RoomsController.cs
+++ b/src/Api/Controllers/RoomsController.cs
@@ -1,10 +1,11 @@
+using Api.Controllers.Payload.Requests.Rooms;
+using Application.Common.Interfaces;
using Application.Common.Models;
using Application.Common.Models.Dtos.Physical;
using Application.Identity;
-using Application.Rooms.Commands.CreateRoom;
-using Application.Rooms.Commands.DisableRoom;
-using Application.Rooms.Commands.RemoveRoom;
-using Application.Rooms.Queries.GetEmptyContainersPaginated;
+using Application.Rooms.Commands;
+using Application.Rooms.Queries;
+using Application.Staffs.Queries;
using Infrastructure.Identity.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -12,44 +13,192 @@ namespace Api.Controllers;
public class RoomsController : ApiControllerBase
{
- [RequiresRole(IdentityData.Roles.Admin)]
- [HttpPost]
+ 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.Status400BadRequest)]
- public async Task>> AddRoom(CreateRoomCommand command)
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task>> GetById(
+ [FromRoute] Guid roomId)
{
- var result = await Mediator.Send(command);
+ 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);
return Ok(Result.Succeed(result));
}
+
+ ///
+ /// Get all rooms paginated
+ ///
+ /// Get all rooms paginated details
+ /// A paginated list of rooms
+ [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,
+ IsAvailable = queryParameters.IsAvailable,
+ 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 empty containers in a room
+ ///
+ ///
+ /// Get empty containers paginated details
+ /// A paginated list of EmptyLockerDto
[RequiresRole(IdentityData.Roles.Staff)]
- [HttpPost("empty-containers")]
+ [HttpPost("{roomId:guid}/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));
}
-
- [HttpPut]
+
+ ///
+ /// 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
+ ///
+ /// Add room details
+ /// A RoomDto of the added room
+ [RequiresRole(IdentityData.Roles.Admin)]
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
+ 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,
+ 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("{roomId:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task>> DisableRoom(DisableRoomCommand command)
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task>> RemoveRoom(
+ [FromRoute] Guid roomId)
{
+ var currentUser = _currentUserService.GetCurrentUser();
+ var command = new RemoveRoom.Command()
+ {
+ CurrentUser = currentUser,
+ RoomId = roomId,
+ };
var result = await Mediator.Send(command);
return Ok(Result.Succeed(result));
}
- [HttpDelete]
+ ///
+ /// Update a room
+ ///
+ /// 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.Status409Conflict)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task>> RemoveRoom(RemoveRoomCommand command)
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ 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));
}
diff --git a/src/Api/Controllers/SharedController.cs b/src/Api/Controllers/SharedController.cs
new file mode 100644
index 00000000..165a9979
--- /dev/null
+++ b/src/Api/Controllers/SharedController.cs
@@ -0,0 +1,201 @@
+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.Entries.Commands;
+using Application.Entries.Queries;
+using Application.Identity;
+using FluentValidation.Results;
+using Application.Users.Queries;
+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;
+ }
+
+ ///
+ /// 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(
+ [FromQuery] GetAllSharedEntriesPaginatedQueryParameters 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));
+ }
+
+
+ ///
+ /// 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>> CreateSharedEntry(
+ [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.")
+ });
+ }
+
+ CreateSharedEntry.Command command;
+
+ if (request.IsDirectory)
+ {
+ command = new CreateSharedEntry.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 CreateSharedEntry.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));
+ }
+
+ ///
+ /// 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(
+ [FromRoute] Guid entryId,
+ [FromQuery] GetAllSharedUsersPaginatedQueryParameters 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));
+ }
+
+
+ ///
+ /// Share an entry to a user
+ ///
+ ///
+ /// an EntryPermissionDto
+ [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));
+ }
+
+ ///
+ /// Get shared entry by Id
+ ///
+ /// an EntryDto
+ [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/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs
index 255335ef..19c196b7 100644
--- a/src/Api/Controllers/StaffsController.cs
+++ b/src/Api/Controllers/StaffsController.cs
@@ -1,7 +1,11 @@
+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.Staffs.Commands.CreateStaff;
-using Application.Users.Queries.Physical;
+using Application.Rooms.Queries;
+using Application.Staffs.Commands;
+using Application.Staffs.Queries;
using Infrastructure.Identity.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -9,13 +13,125 @@ 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)
+ {
+ var query = new GetStaffById.Query()
+ {
+ StaffId = staffId,
+ };
+ var result = await Mediator.Send(query);
+ return Ok(Result.Succeed(result));
+ }
+
+ ///
+ /// Get room by staff id
+ ///
+ /// 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>> GetRoomByStaffId(
+ [FromRoute] Guid staffId)
+ {
+ var currentUser = _currentUserService.GetCurrentUser();
+ var query = new GetRoomByStaffId.Query()
+ {
+ CurrentUser = currentUser,
+ StaffId = staffId,
+ };
+ 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
+ [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task>>> GetAllPaginated(
+ [FromQuery] GetAllStaffsPaginatedQueryParameters queryParameters)
+ {
+ var query = new GetAllStaffsPaginated.Query()
+ {
+ 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));
+ }
+
+ ///
+ /// Assign 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>> CreateStaff([FromBody] CreateStaffCommand command)
+ public async Task>> Assign(
+ [FromBody] AddStaffRequest request)
+ {
+ var currentUser = _currentUserService.GetCurrentUser();
+ var command = new AssignStaff.Command()
+ {
+ CurrentUser = currentUser,
+ RoomId = request.RoomId,
+ StaffId = request.StaffId,
+ };
+ 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
+ /// 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>> 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));
}
diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs
index 33aa1714..aed6dfa8 100644
--- a/src/Api/Controllers/UsersController.cs
+++ b/src/Api/Controllers/UsersController.cs
@@ -1,54 +1,188 @@
+using Api.Controllers.Payload.Requests.Users;
+using Application.Common.Interfaces;
using Application.Common.Models;
using Application.Identity;
-using Application.Users.Commands.CreateUser;
-using Application.Users.Commands.DisableUser;
+using Application.Users.Commands;
using Application.Users.Queries;
-using Application.Users.Queries.GetUsersByName;
using Infrastructure.Identity.Authorization;
-using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers;
public class UsersController : ApiControllerBase
{
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ 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)]
- [ProducesResponseType(StatusCodes.Status409Conflict)]
- public async Task>> CreateUser([FromBody] CreateUserCommand command)
+ public async Task>> GetById([FromRoute] Guid userId)
{
- var result = await Mediator.Send(command);
+ 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);
return Ok(Result.Succeed(result));
}
-
- [Authorize]
+
+ ///
+ /// Get all users paginated
+ ///
+ /// Get all users query parameters
+ /// A paginated list of UserDto
[RequiresRole(IdentityData.Roles.Admin)]
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task>>> GetUsersByName(string? searchTerm, int? page, int? size)
+ public async Task>>> GetAllPaginated(
+ [FromQuery] GetAllUsersPaginatedQueryParameters queryParameters)
{
- var query = new GetUsersByNameQuery
+ var query = new GetAllUsersPaginated.Query()
{
- SearchTerm = searchTerm,
- Page = page,
- Size = size
+ 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));
}
-
- [HttpPost("disable")]
+
+ ///
+ /// 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,
+ SortBy = queryParameters.SortBy,
+ SortOrder = queryParameters.SortOrder,
+ };
+ var result = await Mediator.Send(query);
+ 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.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [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,
+ LastName = request.LastName,
+ Role = request.Role,
+ Position = request.Position,
+ DepartmentId = request.DepartmentId,
+ };
+ 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
[RequiresRole(IdentityData.Roles.Admin)]
+ [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 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));
+ }
+
+ ///
+ /// Update a user
+ ///
+ /// Update user details
+ /// A UserDto of the updated user
+ [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)]
+ [HttpPut("self")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task>> DisableUser([FromBody] DisableUserCommand command)
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task>> UpdateSelf(
+ [FromBody] UpdateSelfRequest request)
{
+ var currentUser = _currentUserService.GetCurrentUser();
+ var command = new UpdateUser.Command()
+ {
+ CurrentUser = currentUser,
+ UserId = currentUser.Id,
+ FirstName = request.FirstName,
+ LastName = request.LastName,
+ Position = currentUser.Position,
+ Role = currentUser.Role,
+ IsActive = currentUser.IsActive,
+ };
var result = await Mediator.Send(command);
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((context, _) =>
+ {
+ ApplicationDbContextSeed.Seed(context, configuration, Log.Logger).Wait();
+ });
}
- else
+
+ if (app.Environment.IsEnvironment("Testing"))
+ {
+ app.MigrateDatabase((_, _) =>
+ {
+ });
+ }
+
+ if (app.Environment.IsEnvironment("Production"))
{
+ app.UseCors(CORSPolicy.Production);
+ app.MigrateDatabase((_, _) =>
+ {
+ // TODO: should only generate admin account
+ });
}
app.UseAuthentication();
diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs
index 194d73ab..a99cbdf5 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 },
};
}
@@ -54,25 +55,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,29 +87,34 @@ 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.Status401Unauthorized;
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
await WriteExceptionMessageAsync(context, ex);
}
private static async void HandleUnauthorizedAccessException(HttpContext context, Exception ex)
{
- context.Response.StatusCode = StatusCodes.Status401Unauthorized;
+ context.Response.StatusCode = StatusCodes.Status403Forbidden;
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);
}
+
+ private static async void HandleNotChangedException(HttpContext context, Exception ex)
+ {
+ context.Response.StatusCode = StatusCodes.Status204NoContent;
+ }
private static async Task WriteExceptionMessageAsync(HttpContext context, Exception ex)
{
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
index 42a37f8f..9c5fb823 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);
@@ -17,17 +16,12 @@
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
- builder.Services.AddApiServices();
-
+ builder.Services.AddApiServices(builder.Configuration);
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/BackgroundWorkers.cs b/src/Api/Services/BackgroundWorkers.cs
new file mode 100644
index 00000000..70c1228f
--- /dev/null
+++ b/src/Api/Services/BackgroundWorkers.cs
@@ -0,0 +1,78 @@
+using Application.Common.Interfaces;
+using Domain.Statuses;
+using Infrastructure.Identity.Authorization;
+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),
+ HandleOverdueRequest(TimeSpan.FromMinutes(2), 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);
+ }
+
+ 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/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs
index 34a4b5be..143498a9 100644
--- a/src/Api/Services/CurrentUserService.cs
+++ b/src/Api/Services/CurrentUserService.cs
@@ -1,30 +1,38 @@
using System.IdentityModel.Tokens.Jwt;
using Application.Common.Interfaces;
-using Application.Identity;
+using Domain.Entities;
+using Microsoft.EntityFrameworkCore;
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(JwtRegisteredClaimNames.Sub));
+ .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)
{
@@ -34,22 +42,66 @@ public string GetRole()
return user.Role;
}
- public string? GetDepartment()
+ public Guid GetDepartmentId()
+ {
+ 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(JwtRegisteredClaimNames.Sub));
+ .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
+ .Include(x => x.Department)
+ .FirstOrDefault(x => x.Username.Equals(userName));
if (user is null)
{
throw new UnauthorizedAccessException();
}
- return user.Department?.Name;
+ return user;
+ }
+
+ public Guid? GetCurrentRoomForStaff()
+ {
+ 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 = _dbContext.Staffs
+ .Include(x => x.User)
+ .Include(x => x.Room)
+ .FirstOrDefault(x => x.Id == userId);
+
+ return staff?.Room?.Id;
+ }
+
+ public Guid? GetCurrentDepartmentForStaff()
+ {
+ 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 = _dbContext.Staffs
+ .Include(x => x.User)
+ .Include(x => x.Room)
+ .FirstOrDefault(x => x.Id == userId);
+
+ return staff?.Room?.DepartmentId;
}
}
\ No newline at end of file
diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json
index 41b82277..7d9b8098 100644
--- a/src/Api/appsettings.Development.json
+++ b/src/Api/appsettings.Development.json
@@ -9,6 +9,21 @@
"TokenLifetime": "00:20:00",
"RefreshTokenLifetimeInDays": 3
},
+ "SecuritySettings": {
+ "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53"
+ },
+
+ "MailSettings": {
+ "ClientUrl": "https://send.api.mailtrap.io/api/send",
+ "Token": "745f040659edff0ce87b545567da72d2",
+ "SenderName": "ProFile",
+ "SenderEmail": "profile@ezarp.dev",
+ "TemplateUuids": {
+ "ResetPassword": "9d6a8f25-65e9-4819-be7d-106ce077acf1",
+ "ShareEntry": "ad69df89-885a-48fb-b8f6-6d06af1a54e3",
+ "Request": "bce7e60c-d848-4f80-af96-cccd264dcc32"
+ }
+ },
"Seed": true,
"Serilog" : {
"MinimumLevel" : {
@@ -17,8 +32,48 @@
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Authentication": "Debug",
- "System": "Warning"
+ "System": "Warning",
+ "Infrastructure.Identity.Authentication": "Warning"
}
- }
+ },
+ "WriteTo": [
+ {
+ "Name": "PostgreSQL",
+ "Args": {
+ "connectionString": "Server=postgres;Port=5432;Database=mydb;User ID=profiletest;Password=supasecured",
+ "tableName": "Logs",
+ "schemaName": null,
+ "needAutoCreateTable": true,
+ "loggerColumnOptions": {
+ "Id": "IdAutoIncrement",
+ "Template": "Message",
+ "Time": "Timestamp",
+ "Event": "LogEvent",
+ "Level": "LevelAsText",
+ "Message": "RenderedMessage"
+ },
+ "loggerPropertyColumnOptions": {
+ "ObjectType": {
+ "Name": "ObjectType",
+ "Format": "{0}",
+ "WriteMethod": "Raw",
+ "DbType": "Text"
+ },
+ "ObjectId": {
+ "Name": "ObjectId",
+ "Format": "{0}",
+ "WriteMethod": "Raw",
+ "DbType": "Uuid"
+ },
+ "UserId": {
+ "Name": "UserId",
+ "Format": "{0}",
+ "WriteMethod": "Raw",
+ "DbType": "Uuid"
+ }
+ }
+ }
+ }
+ ]
}
-}
+}
\ No newline at end of file
diff --git a/src/Api/appsettings.Testing.json b/src/Api/appsettings.Testing.json
new file mode 100644
index 00000000..aafebb14
--- /dev/null
+++ b/src/Api/appsettings.Testing.json
@@ -0,0 +1,23 @@
+{
+ "JweSettings": {
+ "SigningKeyId": "4bd28be8eac5414fb01c5cbe343b50144bd2",
+ "EncryptionKeyId": "4bd28be8eac5414fb01c5cbe343b5014",
+ "TokenLifetime": "00:20:00",
+ "RefreshTokenLifetimeInDays": 3
+ },
+ "SecuritySettings": {
+ "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53"
+ },
+ "Seed": true,
+ "Serilog" : {
+ "MinimumLevel" : {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.AspNetCore.Authentication": "Debug",
+ "System": "Warning"
+ }
+ }
+ }
+}
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
index ecc9e09f..8853c8ca 100644
--- a/src/Application/Application.csproj
+++ b/src/Application/Application.csproj
@@ -11,7 +11,11 @@
+
+
+
+
@@ -19,8 +23,4 @@
-
-
-
-
diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs
new file mode 100644
index 00000000..60a9e295
--- /dev/null
+++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs
@@ -0,0 +1,146 @@
+using Application.Common.Exceptions;
+using Application.Common.Extensions;
+using Application.Common.Interfaces;
+using Application.Common.Logging;
+using Application.Common.Models.Dtos.Physical;
+using AutoMapper;
+using Domain.Enums;
+using Domain.Statuses;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+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;
+ 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)
+ {
+ 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));
+
+ 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;
+
+ using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogApproveBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString());
+ }
+ }
+
+ if (request.Decision.IsRejection())
+ {
+ borrowRequest.Status = BorrowRequestStatus.Rejected;
+
+ using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogRejectBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString());
+ }
+ }
+
+ borrowRequest.StaffReason = request.StaffReason;
+ borrowRequest.LastModified = localDateTimeNow;
+ borrowRequest.LastModifiedBy = request.CurrentUserId;
+
+ 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..7c7eddad
--- /dev/null
+++ b/src/Application/Borrows/Commands/BorrowDocument.cs
@@ -0,0 +1,172 @@
+using Application.Common.Exceptions;
+using Application.Common.Interfaces;
+using Application.Common.Logging;
+using Application.Common.Models.Dtos.Physical;
+using Application.Common.Models.Operations;
+using AutoMapper;
+using Domain.Entities.Physical;
+using Domain.Events;
+using Domain.Statuses;
+using FluentValidation;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NodaTime;
+
+namespace Application.Borrows.Commands;
+
+public class BorrowDocument
+{
+ public class Validator : AbstractValidator
+ {
+ public Validator()
+ {
+ RuleLevelCascadeMode = CascadeMode.Stop;
+
+ RuleFor(x => x.BorrowReason)
+ .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 BorrowReason { get; init; } = null!;
+ }
+
+ public class CommandHandler : IRequestHandler
+ {
+ private readonly IApplicationDbContext _context;
+ 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, ILogger logger)
+ {
+ _context = context;
+ _mapper = mapper;
+ _permissionManager = permissionManager;
+ _dateTimeProvider = dateTimeProvider;
+ _logger = logger;
+ }
+
+ 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)
+ .Include(x => x.Importer)
+ .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(_dateTimeProvider.DateTimeNow);
+ var borrowFromTime = LocalDateTime.FromDateTime(request.BorrowFrom);
+ var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo);
+ var existedBorrows = _context.Borrows
+ .Include(x => x.Borrower)
+ .Where(x =>
+ x.Document.Id == request.DocumentId
+ && (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 (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 ((borrow.Status
+ is BorrowRequestStatus.Approved
+ or BorrowRequestStatus.CheckedOut)
+ && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime))
+ {
+ throw new ConflictException("This document cannot be borrowed.");
+ }
+ }
+
+ var entity = new Borrow()
+ {
+ Borrower = user,
+ Document = document,
+ 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 (document.ImporterId != request.BorrowerId && !isGranted)
+ {
+ throw new UnauthorizedAccessException("You don't have permission to borrow this document.");
+ }
+ 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))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogBorrowDocument(_logger, document.Id.ToString(), result.Entity.Id.ToString());
+ }
+ 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..135d05ef
--- /dev/null
+++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs
@@ -0,0 +1,73 @@
+using Application.Common.Exceptions;
+using Application.Common.Interfaces;
+using Application.Common.Logging;
+using Application.Common.Models.Dtos.Physical;
+using AutoMapper;
+using Domain.Statuses;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NodaTime;
+
+namespace Application.Borrows.Commands;
+
+public class CancelBorrowRequest
+{
+ public record Command : IRequest
+ {
+ public Guid CurrentUserId { get; init; }
+ public Guid BorrowId { get; init; }
+ }
+
+ 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)
+ {
+ _context = context;
+ _mapper = mapper;
+ _dateTimeProvider = dateTimeProvider;
+ _logger = logger;
+ }
+
+ 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 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);
+
+ borrowRequest.Status = BorrowRequestStatus.Cancelled;
+ var result = _context.Borrows.Update(borrowRequest);
+ await _context.SaveChangesAsync(cancellationToken);
+ using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUserId))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogCancelBorrowRequest(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString());
+ }
+ 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..0dd95d0a
--- /dev/null
+++ b/src/Application/Borrows/Commands/CheckoutDocument.cs
@@ -0,0 +1,98 @@
+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.Statuses;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NodaTime;
+
+namespace Application.Borrows.Commands;
+
+public class CheckoutDocument
+{
+ public record Command : IRequest
+ {
+ public User CurrentStaff { get; init; } = null!;
+ public Guid BorrowId { get; init; }
+ }
+
+ 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)
+ {
+ _context = context;
+ _mapper = mapper;
+ _dateTimeProvider = dateTimeProvider;
+ _logger = logger;
+ }
+
+ 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 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.");
+ }
+
+ 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 result = _context.Borrows.Update(borrowRequest);
+ _context.Documents.Update(borrowRequest.Document);
+ await _context.SaveChangesAsync(cancellationToken);
+ using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentStaff.Id))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogCheckoutDocument(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString());
+ }
+ return _mapper.Map(result.Entity);
+ }
+ }
+}
\ No newline at end of file
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
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/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs
new file mode 100644
index 00000000..2ee6da97
--- /dev/null
+++ b/src/Application/Borrows/Commands/ReturnDocument.cs
@@ -0,0 +1,99 @@
+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.Statuses;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NodaTime;
+
+namespace Application.Borrows.Commands;
+
+public class ReturnDocument
+{
+ 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;
+ 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)
+ {
+ 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);
+ 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.");
+ }
+
+ 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 = localDateTimeNow;
+
+ var result = _context.Borrows.Update(borrowRequest);
+ _context.Documents.Update(borrowRequest.Document);
+ await _context.SaveChangesAsync(cancellationToken);
+ using (Logging.PushProperties("BorrowRequest", borrowRequest.Id, request.CurrentUser.Id))
+ {
+ Common.Extensions.Logging.BorrowLogExtensions.LogReturnDocument(_logger, borrowRequest.Document.Id.ToString(), borrowRequest.Id.ToString());
+ }
+ 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..cdbf0d9e
--- /dev/null
+++ b/src/Application/Borrows/Commands/UpdateBorrow.cs
@@ -0,0 +1,124 @@
+using Application.Common.Exceptions;
+using Application.Common.Interfaces;
+using Application.Common.Logging;
+using Application.Common.Models.Dtos.Physical;
+using AutoMapper;
+using Domain.Entities;
+using Domain.Statuses;
+using FluentValidation;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using NodaTime;
+
+namespace Application.Borrows.Commands;
+
+public class UpdateBorrow
+{
+ public class Validator : AbstractValidator
+ {
+ public Validator()
+ {
+ RuleLevelCascadeMode = CascadeMode.Stop;
+
+ RuleFor(x => x.BorrowReason)
+ .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