├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── TechTrack.Api ├── Controllers │ ├── ApiController.cs │ ├── EquipmentController.cs │ └── UsersController.cs ├── DataSeeder.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── TechTrack.Api.csproj ├── TechTrack.Api.http ├── appsettings.Development.json └── appsettings.json ├── TechTrack.Application ├── Constants │ └── EquipmentSortingConstants.cs ├── Equipments │ ├── Commands │ │ ├── CreateEquipment │ │ │ ├── CreateEquipmentCommand.cs │ │ │ ├── CreateEquipmentCommandHandler.cs │ │ │ └── CreateEquipmentCommandValidator.cs │ │ ├── RetireEquipment │ │ │ ├── RetireEquipmentCommand.cs │ │ │ ├── RetireEquipmentCommandHandler.cs │ │ │ └── RetireEquipmentCommandValidator.cs │ │ └── UpdateEquipment │ │ │ ├── UpdateEquipmentCommand.cs │ │ │ ├── UpdateEquipmentCommandHandler.cs │ │ │ └── UpdateEquipmentCommandValidator.cs │ ├── Extensions │ │ └── EquipmentExtensions.cs │ └── Queries │ │ ├── GetEquipment │ │ ├── GetEquipmentQuery.cs │ │ ├── GetEquipmentQueryHandler.cs │ │ └── GetEquipmentQueryValidator.cs │ │ └── GetEquipments │ │ ├── GetEquipmentsQuery.cs │ │ ├── GetEquipmentsQueryHandler.cs │ │ └── GetEquipmentsQueryValidator.cs ├── EventHandlers │ ├── EquipmentCreatedEventHandler.cs │ ├── EquipmentRetiredEventHandler.cs │ └── EquipmentUpdatedEventHandler.cs ├── Events │ ├── DomainEventNotification.cs │ ├── DomainEventService.cs │ ├── EquipmentCreated.cs │ ├── EquipmentRetired.cs │ └── EquipmentUpdated.cs ├── Helpers │ └── PredicateBuilder.cs ├── Interfaces │ ├── Equipments │ │ ├── IEquipmentReadRepository.cs │ │ ├── IEquipmentWriteRepository.cs │ │ └── IEquipmentsHttpClientService.cs │ └── Users │ │ ├── IUserHttpClientService.cs │ │ ├── IUsersReadRepository.cs │ │ └── IUsersWriteRepository.cs ├── MappingProfile.cs ├── TechTrack.Application.csproj └── Users │ ├── Commands │ ├── CreateUserCommand.cs │ └── CreateUserCommandHandler.cs │ └── Queries │ ├── GetUserWithEquipmentQuery.cs │ └── GetUsersWithEquipmentsQueryHandler.cs ├── TechTrack.Common ├── Dtos │ ├── Equipments │ │ ├── EquipmentDto.cs │ │ ├── EquipmentFilterDto.cs │ │ ├── EquipmentForCreationDto.cs │ │ └── EquipmentForUpdateDto.cs │ └── Users │ │ └── UserForCreationDto.cs ├── Interfaces │ ├── HttpClients │ │ ├── IEquipmentsHttpClientService.cs │ │ └── IUserHttpClientService.cs │ └── IDomainEventService.cs ├── Pagination │ ├── PaginatedResult.cs │ └── PaginationFilter.cs ├── TechTrack.Common.csproj └── ViewModels │ ├── Equipments │ ├── EquipmentInputVm.cs │ └── EquipmentOutputVm.cs │ └── Users │ └── UserWithEquipmentVm.cs ├── TechTrack.Domain ├── Common │ ├── DomainEvent.cs │ ├── Entity.cs │ ├── Extensions │ │ └── EnumExtensions.cs │ ├── Interfaces │ │ ├── IAggregateRoot.cs │ │ └── IHasDomainEvent.cs │ └── ValueObject.cs ├── Enums │ └── EquipmentStatus.cs ├── Models │ ├── Equipment.cs │ └── User.cs └── TechTrack.Domain.csproj ├── TechTrack.Infrastructure └── TechTrack.Infrastructure.csproj ├── TechTrack.Persistence ├── Configurations │ ├── EquipmentConfiguration.cs │ └── UserConfiguration.cs ├── DatabaseContext │ ├── ReadDbContext.cs │ └── WriteDbContext.cs ├── Migrations │ ├── Read │ │ ├── 20240419195506_InitialMigration.Designer.cs │ │ ├── 20240419195506_InitialMigration.cs │ │ ├── 20240426191153_UpdatedSerialNumber.Designer.cs │ │ ├── 20240426191153_UpdatedSerialNumber.cs │ │ └── ReadDbContextModelSnapshot.cs │ └── Write │ │ ├── 20240419195535_InitialMigration.Designer.cs │ │ ├── 20240419195535_InitialMigration.cs │ │ ├── 20240426191227_UpdatedSerialNumber.Designer.cs │ │ ├── 20240426191227_UpdatedSerialNumber.cs │ │ └── WriteDbContextModelSnapshot.cs ├── Repositories │ ├── EquipmentReadRepository.cs │ ├── EquipmentWriteRepository.cs │ ├── UsersReadRepository.cs │ └── UsersWriteRepository.cs └── TechTrack.Persistence.csproj ├── TechTrack.UnitTests ├── Application │ └── Equipments │ │ ├── Commands │ │ ├── CreateEquipment │ │ │ ├── CreateEquipmentCommandHandlerTests.cs │ │ │ └── CreateEquipmentCommandValidatorTests.cs │ │ ├── RetireEquipment │ │ │ ├── RetireEquipmentCommandHandlerTests.cs │ │ │ └── RetireEquipmentCommandValidatorTests.cs │ │ └── UpdateEquipment │ │ │ ├── UpdateEquipmentCommandHandlerTests.cs │ │ │ └── UpdateEquipmentCommandValidatorTests.cs │ │ └── Queries │ │ ├── GetEquipment │ │ └── GetEquipmentQueryHandlerTests.cs │ │ └── GetEquipments │ │ └── GetEquipmentsQueryHandlerTests.cs ├── GlobalUsings.cs └── TechTrack.UnitTests.csproj ├── TechTrack.sln └── Techtrack.Ui ├── Techtrack.Ui.Client ├── Components │ └── Pages │ │ ├── Auth.razor │ │ ├── Equipments │ │ ├── AddEquipment.razor │ │ └── Equipment.razor │ │ ├── PaginatedList.razor │ │ └── Users │ │ └── UsersWithEquipments.razor ├── PersistentAuthenticationStateProvider.cs ├── Program.cs ├── RedirectToLogin.razor ├── Services │ ├── EquipmentsHttpClientService.cs │ └── UsersHttpClientService.cs ├── Techtrack.Ui.Client.csproj ├── UserInfo.cs ├── _Imports.razor └── wwwroot │ ├── appsettings.Development.json │ └── appsettings.json └── Techtrack.Ui ├── Components ├── Account │ ├── IdentityComponentsEndpointRouteBuilderExtensions.cs │ ├── IdentityNoOpEmailSender.cs │ ├── IdentityRedirectManager.cs │ ├── IdentityUserAccessor.cs │ ├── Pages │ │ ├── ConfirmEmail.razor │ │ ├── ConfirmEmailChange.razor │ │ ├── ExternalLogin.razor │ │ ├── ForgotPassword.razor │ │ ├── ForgotPasswordConfirmation.razor │ │ ├── InvalidPasswordReset.razor │ │ ├── InvalidUser.razor │ │ ├── Lockout.razor │ │ ├── Login.razor │ │ ├── LoginWith2fa.razor │ │ ├── LoginWithRecoveryCode.razor │ │ ├── Manage │ │ │ ├── ChangePassword.razor │ │ │ ├── DeletePersonalData.razor │ │ │ ├── Disable2fa.razor │ │ │ ├── Email.razor │ │ │ ├── EnableAuthenticator.razor │ │ │ ├── ExternalLogins.razor │ │ │ ├── GenerateRecoveryCodes.razor │ │ │ ├── Index.razor │ │ │ ├── PersonalData.razor │ │ │ ├── ResetAuthenticator.razor │ │ │ ├── SetPassword.razor │ │ │ ├── TwoFactorAuthentication.razor │ │ │ └── _Imports.razor │ │ ├── Register.razor │ │ ├── RegisterConfirmation.razor │ │ ├── ResendEmailConfirmation.razor │ │ ├── ResetPassword.razor │ │ ├── ResetPasswordConfirmation.razor │ │ └── _Imports.razor │ ├── PersistingRevalidatingAuthenticationStateProvider.cs │ └── Shared │ │ ├── AccountLayout.razor │ │ ├── ExternalLoginPicker.razor │ │ ├── ManageLayout.razor │ │ ├── ManageNavMenu.razor │ │ ├── ShowRecoveryCodes.razor │ │ └── StatusMessage.razor ├── App.razor ├── Layout │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ ├── NavMenu.razor.css │ └── PaginatedList.razor ├── Pages │ ├── Equipments │ │ └── UpdateEquipment.razor │ ├── Error.razor │ └── Home.razor ├── Routes.razor └── _Imports.razor ├── Data ├── ApplicationDbContext.cs ├── ApplicationUser.cs └── Migrations │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ ├── 00000000000000_CreateIdentitySchema.cs │ └── ApplicationDbContextModelSnapshot.cs ├── DataSeeder.cs ├── Program.cs ├── Properties ├── launchSettings.json ├── serviceDependencies.json └── serviceDependencies.local.json ├── Techtrack.Ui.csproj ├── appsettings.Development.json ├── appsettings.json └── wwwroot ├── app.css ├── bootstrap ├── bootstrap.min.css └── bootstrap.min.css.map └── favicon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Theodor Sioustis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # TechTrack 3 | 4 | TechTrack is an app that tracks the equipment assigned to users. It was made to showcase Clean Architecture, CQRS principles and the newest Blazor features. 5 | 6 | ## Features 7 | 8 | - **TechTrack.Api**: Backend API to handle all data processing and business logic. 9 | - **TechTrack.Application**: Core application logic and services. 10 | - **TechTrack.Common**: Shared utilities and helper functions. 11 | - **TechTrack.Domain**: Domain models and entities. 12 | - **TechTrack.Persistence**: Data persistence and repository implementations. 13 | - **TechTrack.UnitTests**: Unit tests for ensuring code quality. 14 | - **Techtrack.Ui**: Frontend user interface built with Blazor. 15 | 16 | ## Technologies 17 | 18 | - **C#**: Backend development. 19 | - **Blazor**: Frontend development. 20 | 21 | ## Getting Started 22 | 23 | 1. Clone the repository: 24 | `git clone https://github.com/TSiustis/TechTrack.git` 25 | 26 | 2. Navigate to the project directory and open the solution file `TechTrack.sln` in Visual Studio. 27 | 3. Run the update database commands in Package Manager Console: 28 | Update-Database -context ReadDbContext 29 | Update-Database -context WriteDbContext. 30 | 5. Build and run the solution. 31 | 32 | ## Contributing 33 | 34 | Contributions are welcome! Please fork the repository and create a pull request with your changes. 35 | -------------------------------------------------------------------------------- /TechTrack.Api/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace TechTrack.Api.Controllers; 6 | 7 | [ApiController] 8 | [ApiVersion("1.0")] 9 | [Produces("application/json")] 10 | [Route("api/v1")] 11 | public abstract class ApiController : ControllerBase 12 | { 13 | private IMediator _mediator; 14 | 15 | protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService(); 16 | } 17 | -------------------------------------------------------------------------------- /TechTrack.Api/Controllers/EquipmentController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using TechTrack.Common.Pagination; 3 | using TechTrack.Application.Equipments.Commands.CreateEquipment; 4 | using TechTrack.Application.Equipments.Commands.DeleteEquipment; 5 | using TechTrack.Application.Equipments.Commands.UpdateEquipment; 6 | using TechTrack.Common.Dtos.Equipments; 7 | using TechTrack.Application.Equipments.Queries.GetEquipment; 8 | using TechTrack.Application.Equipments.Queries.GetEquipments; 9 | using TechTrack.Common.ViewModel.Equipments; 10 | 11 | namespace TechTrack.Api.Controllers 12 | { 13 | public class EquipmentController : ApiController 14 | { 15 | /// 16 | /// Gets the list of equipments for specified filters. 17 | /// 18 | /// Additional search filters to be applied. 19 | /// Paginated list of equipments. 20 | [HttpGet("equipments")] 21 | [ProducesResponseType(typeof(PaginatedResult), StatusCodes.Status200OK)] 22 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 23 | public async Task>> GetEquipmentsAsync( 24 | [FromQuery] EquipmentInputVm equipmentsFilter) 25 | { 26 | var equipments = await Mediator.Send(new GetEquipmentsQuery(equipmentsFilter)); 27 | 28 | return Ok(equipments); 29 | } 30 | 31 | /// 32 | /// Gets the equipment for a specified guid. 33 | /// 34 | /// The guid. 35 | /// Equipment. 36 | [HttpGet("equipment/{id:Guid}")] 37 | [ProducesResponseType(typeof(EquipmentDto), StatusCodes.Status200OK)] 38 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 39 | public async Task> GetEquipmentAsync(Guid id) 40 | { 41 | var equipment = await Mediator.Send(new GetEquipmentQuery(id)); 42 | 43 | return Ok(equipment); 44 | } 45 | 46 | /// 47 | /// Creates an equipment. 48 | /// 49 | /// The created equipment. 50 | /// 201 Created. 51 | [HttpPost("equipments")] 52 | [ProducesResponseType(StatusCodes.Status201Created)] 53 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 54 | public async Task CreateEquipmentAsync( 55 | [FromBody] EquipmentForCreationDto equipmentDto) 56 | { 57 | await Mediator.Send(new CreateEquipmentCommand(equipmentDto)); 58 | 59 | return Created(); 60 | } 61 | 62 | /// 63 | /// Updates equipment for specified id. 64 | /// 65 | /// The id. 66 | /// The updated equipment. 67 | /// 200 OK. 68 | [HttpPut("equipments/{id:Guid}")] 69 | [ProducesResponseType(StatusCodes.Status200OK)] 70 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 71 | public async Task UpdateEquipmentAsync( 72 | Guid id, 73 | [FromBody] EquipmentForUpdateDto equipmentDto) 74 | { 75 | await Mediator.Send(new UpdateEquipmentCommand(id, equipmentDto)); 76 | 77 | return Ok(); 78 | } 79 | 80 | /// 81 | /// Retires an equipment for a specified id. 82 | /// 83 | /// The id. 84 | /// 204 No content. 85 | [HttpPut("equipments/{id:Guid}/retire")] 86 | [ProducesResponseType(StatusCodes.Status204NoContent)] 87 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 88 | public async Task RetireEquipmentAsync(Guid id) 89 | { 90 | await Mediator.Send(new RetireEquipmentCommand(id)); 91 | 92 | return NoContent(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /TechTrack.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using global::TechTrack.Common.ViewModel.Users; 2 | using Microsoft.AspNetCore.Mvc; 3 | using TechTrack.Application.Users.Commands; 4 | using TechTrack.Application.Users.Queries; 5 | using TechTrack.Common.Dtos.Users; 6 | 7 | namespace TechTrack.Api.Controllers 8 | { 9 | public class UsersController : ApiController 10 | { 11 | /// 12 | /// Gets the list of users with associated equipments. 13 | /// 14 | /// List of users. 15 | [HttpGet("users/with-equipments")] 16 | [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 18 | public async Task>> GetUsersWithEquipments() 19 | { 20 | var userWithquipments = await Mediator.Send(new GetUsersWithEquipmentsQuery()); 21 | 22 | return Ok(userWithquipments); 23 | } 24 | 25 | [HttpPost] 26 | public async Task Create([FromBody] UserForCreationDto createUserDto) 27 | { 28 | var command = new CreateUserCommand(createUserDto); 29 | 30 | var userId = await Mediator.Send(command); 31 | 32 | return Ok(userId); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TechTrack.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using MediatR.Pipeline; 2 | using MediatR; 3 | using Microsoft.EntityFrameworkCore; 4 | using TechTrack.Api; 5 | using TechTrack.Application; 6 | using TechTrack.Application.Interfaces.Equipments; 7 | using TechTrack.Persistence.DatabaseContext; 8 | using TechTrack.Persistence.Repositories; 9 | using TechTrack.Application.Equipments.Queries.GetEquipments; 10 | using TechTrack.Application.Common.Interfaces; 11 | using TechTrack.Application.Events; 12 | using System.Text.Json.Serialization; 13 | using TechTrack.Application.Interfaces.Users; 14 | 15 | public class Program 16 | { 17 | private static async Task Main(string[] args) 18 | { 19 | var builder = WebApplication.CreateBuilder(args); 20 | 21 | // Add services to the container. 22 | builder.Services.AddDbContext(options => 23 | options.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString"))); 24 | builder.Services.AddDbContext(options => 25 | options.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString"))); 26 | 27 | builder.Services.AddScoped(); 28 | builder.Services.AddScoped(); 29 | builder.Services.AddScoped(); 30 | builder.Services.AddScoped(); 31 | 32 | builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); 33 | builder.Services.AddControllers() 34 | .AddJsonOptions(options => 35 | { 36 | options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 37 | }); 38 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 39 | builder.Services.AddEndpointsApiExplorer(); 40 | builder.Services.AddSwaggerGen(); 41 | builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); 42 | builder.Services.AddMediatR(cfg => 43 | cfg.RegisterServicesFromAssembly(typeof(GetEquipmentsQuery).Assembly)); 44 | builder.Services.AddScoped(); 45 | builder.Services.AddApiVersioning(); 46 | 47 | string? origins = "origins"; 48 | 49 | builder.Services.AddCors(options => 50 | { 51 | options.AddPolicy(origins, 52 | policy => 53 | { 54 | policy.AllowAnyOrigin() 55 | .AllowAnyHeader() 56 | .AllowAnyMethod(); 57 | }); 58 | }); 59 | 60 | var app = builder.Build(); 61 | 62 | // Configure the HTTP request pipeline. 63 | if (app.Environment.IsDevelopment()) 64 | { 65 | app.UseSwagger(); 66 | app.UseSwaggerUI(); 67 | } 68 | 69 | app.UseHttpsRedirection(); 70 | 71 | app.UseAuthorization(); 72 | 73 | app.MapControllers(); 74 | 75 | if(app.Environment.IsDevelopment()) 76 | { 77 | await DataSeeder.SeedDataAsync(app.Services); 78 | } 79 | 80 | app.UseCors(origins); 81 | 82 | app.Run(); 83 | } 84 | } -------------------------------------------------------------------------------- /TechTrack.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:17750", 8 | "sslPort": 44310 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5241", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7212;http://localhost:5241", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TechTrack.Api/TechTrack.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TechTrack.Api/TechTrack.Api.http: -------------------------------------------------------------------------------- 1 | @TechTrack.Api_HostAddress = http://localhost:5241 2 | 3 | GET {{TechTrack.Api_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /TechTrack.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /TechTrack.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=aspnet-TechTrack-073d6090-8e71-47db-9003-2d90f04fa7d6;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /TechTrack.Application/Constants/EquipmentSortingConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using TechTrack.Common.Dtos.Equipments; 3 | using TechTrack.Domain.Models; 4 | 5 | namespace TechTrack.Application.Constants; 6 | public static class EquipmentSortingConstants 7 | { 8 | public const string Name = "Name"; 9 | public const string Type = "Type"; 10 | public const string SerialNumber = "SerialNumber"; 11 | public const string Status = "Status"; 12 | public const string AssignmentDate = "AssignmentDate"; 13 | public const string ReturnDate = "ReturnDate"; 14 | 15 | public static readonly IReadOnlyDictionary>> SortExpressions = 16 | new Dictionary>> 17 | { 18 | { Name, equipment => equipment.Name }, 19 | { Type, equipment => equipment.Type }, 20 | { SerialNumber, equipment => equipment.SerialNumber }, 21 | { Status, equipment => equipment.Status }, 22 | { AssignmentDate, equipment => equipment.AssignmentDate }, 23 | { ReturnDate, equipment => equipment.ReturnDate } 24 | }; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/CreateEquipment/CreateEquipmentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.Dtos.Equipments; 3 | 4 | namespace TechTrack.Application.Equipments.Commands.CreateEquipment 5 | { 6 | public class CreateEquipmentCommand : IRequest 7 | { 8 | public CreateEquipmentCommand(EquipmentForCreationDto equipmentForCreationDto) 9 | { 10 | EquipmentForCreationDto = equipmentForCreationDto; 11 | } 12 | 13 | public EquipmentForCreationDto EquipmentForCreationDto { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/CreateEquipment/CreateEquipmentCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using TechTrack.Application.Events; 4 | using TechTrack.Application.Interfaces.Equipments; 5 | using TechTrack.Domain.Models; 6 | 7 | namespace TechTrack.Application.Equipments.Commands.CreateEquipment 8 | { 9 | public class CreateEquipmentCommandHandler : IRequestHandler 10 | { 11 | private readonly IEquipmentWriteRepository _equipmentRepository; 12 | private readonly IMapper _mapper; 13 | 14 | public CreateEquipmentCommandHandler(IEquipmentWriteRepository equipmentRepository, IMapper mapper) 15 | { 16 | _equipmentRepository = equipmentRepository; 17 | _mapper = mapper; 18 | } 19 | 20 | public async Task Handle(CreateEquipmentCommand request, CancellationToken cancellationToken) 21 | { 22 | var equipment = _mapper.Map(request.EquipmentForCreationDto); 23 | 24 | equipment.DomainEvents.Add(new EquipmentCreated(equipment)); 25 | 26 | _equipmentRepository.Add(equipment); 27 | 28 | await _equipmentRepository.SaveChangesAsync(cancellationToken); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/CreateEquipment/CreateEquipmentCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TechTrack.Application.Equipments.Commands.CreateEquipment 4 | { 5 | public class CreateEquipmentCommandValidator : AbstractValidator 6 | { 7 | public CreateEquipmentCommandValidator() 8 | { 9 | RuleFor(command => command.EquipmentForCreationDto. 10 | AssignedToUserId) 11 | .NotNull(); 12 | 13 | RuleFor(command => command.EquipmentForCreationDto. 14 | ReturnDate) 15 | .NotEmpty(); 16 | 17 | RuleFor(command => command.EquipmentForCreationDto. 18 | Name) 19 | .NotEmpty() 20 | .MaximumLength(500); 21 | 22 | RuleFor(command => command.EquipmentForCreationDto. 23 | SerialNumber) 24 | .NotEmpty() 25 | .MaximumLength(24); 26 | 27 | RuleFor(command => command.EquipmentForCreationDto. 28 | Type) 29 | .NotEmpty() 30 | .MaximumLength(50); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/RetireEquipment/RetireEquipmentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace TechTrack.Application.Equipments.Commands.DeleteEquipment 4 | { 5 | public class RetireEquipmentCommand : IRequest 6 | { 7 | public RetireEquipmentCommand(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/RetireEquipment/RetireEquipmentCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Events; 3 | using TechTrack.Application.Interfaces.Equipments; 4 | 5 | namespace TechTrack.Application.Equipments.Commands.DeleteEquipment 6 | { 7 | public class RetireEquipmentCommandHandler : IRequestHandler 8 | { 9 | private readonly IEquipmentWriteRepository _equipmentRepository; 10 | 11 | public RetireEquipmentCommandHandler(IEquipmentWriteRepository equipmentRepository) 12 | { 13 | _equipmentRepository = equipmentRepository; 14 | } 15 | 16 | public async Task Handle(RetireEquipmentCommand request, CancellationToken cancellationToken) 17 | { 18 | var equipment = await _equipmentRepository.GetEquipmentAsync(request.Id, cancellationToken); 19 | 20 | if(equipment is null) 21 | { 22 | throw new KeyNotFoundException($"Equipment with ID {request.Id} does not exist."); 23 | } 24 | 25 | equipment.DomainEvents.Add(new EquipmentRetired(equipment.Id)); 26 | 27 | _equipmentRepository.Retire(request.Id); 28 | 29 | await _equipmentRepository.SaveChangesAsync(cancellationToken); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/RetireEquipment/RetireEquipmentCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TechTrack.Application.Equipments.Commands.DeleteEquipment 4 | { 5 | public class RetireEquipmentCommandValidator : AbstractValidator 6 | { 7 | public RetireEquipmentCommandValidator() 8 | { 9 | RuleFor(command => command.Id) 10 | .NotNull() 11 | .NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/UpdateEquipment/UpdateEquipmentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.Dtos.Equipments; 3 | 4 | namespace TechTrack.Application.Equipments.Commands.UpdateEquipment 5 | { 6 | public class UpdateEquipmentCommand : IRequest 7 | { 8 | public UpdateEquipmentCommand(Guid id, EquipmentForUpdateDto equipment) 9 | { 10 | Id = id; 11 | Equipment = equipment; 12 | } 13 | 14 | public Guid Id { get; set; } 15 | public EquipmentForUpdateDto Equipment { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/UpdateEquipment/UpdateEquipmentCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using TechTrack.Application.Events; 4 | using TechTrack.Application.Interfaces.Equipments; 5 | 6 | namespace TechTrack.Application.Equipments.Commands.UpdateEquipment 7 | { 8 | public class UpdateEquipmentCommandHandler : IRequestHandler 9 | { 10 | private readonly IEquipmentWriteRepository _equipmentRepository; 11 | private readonly IMapper _mapper; 12 | 13 | public UpdateEquipmentCommandHandler(IEquipmentWriteRepository equipmentRepository, IMapper mapper) 14 | { 15 | _equipmentRepository = equipmentRepository; 16 | _mapper = mapper; 17 | } 18 | 19 | public async Task Handle(UpdateEquipmentCommand request, CancellationToken cancellationToken) 20 | { 21 | 22 | var equipment = await _equipmentRepository.GetEquipmentAsync(request.Id, cancellationToken); 23 | 24 | if (equipment is null) 25 | { 26 | throw new KeyNotFoundException($"Equipment with id {request.Id} does not exist."); 27 | } 28 | 29 | _mapper.Map(request.Equipment, equipment); 30 | 31 | equipment.Id = request.Id; 32 | 33 | equipment.DomainEvents.Add(new EquipmentUpdated(equipment)); 34 | 35 | _equipmentRepository.Update(equipment); 36 | 37 | await _equipmentRepository.SaveChangesAsync(cancellationToken); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Commands/UpdateEquipment/UpdateEquipmentCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using TechTrack.Application.Interfaces.Equipments; 3 | 4 | namespace TechTrack.Application.Equipments.Commands.UpdateEquipment 5 | { 6 | public class UpdateEquipmentCommandValidator : AbstractValidator 7 | { 8 | private readonly IEquipmentReadRepository _equipmentReadRepository; 9 | 10 | 11 | public UpdateEquipmentCommandValidator(IEquipmentReadRepository equipmentReadRepository) 12 | { 13 | _equipmentReadRepository = equipmentReadRepository; 14 | 15 | RuleFor(command => command.Id) 16 | .NotEmpty() 17 | .MustAsync(EquipmentExists) 18 | .WithMessage(command => $"Equipment with ID {command.Id} does not exist."); 19 | } 20 | 21 | private async Task EquipmentExists(Guid id, CancellationToken cancellationToken) 22 | { 23 | var equipment = await _equipmentReadRepository.GetByIdAsync(id, cancellationToken); 24 | 25 | return equipment is not null; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Extensions/EquipmentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using TechTrack.Application.Helpers; 3 | using TechTrack.Domain.Enums; 4 | using TechTrack.Domain.Models; 5 | 6 | namespace TechTrack.Application.Equipments.Extensions; 7 | public static class EquipmentExtensions 8 | { 9 | public static Expression> HasGuidEqualTo( 10 | this Expression> predicate, Guid guid) 11 | { 12 | if (guid == Guid.Empty) 13 | { 14 | return predicate; 15 | } 16 | return predicate.And(equipment => equipment.Id == guid); 17 | } 18 | 19 | public static Expression> AndNameEqualTo( 20 | this Expression> predicate, string name) 21 | { 22 | if (string.IsNullOrEmpty(name)) 23 | { 24 | return predicate; 25 | } 26 | return predicate.And(equipment => equipment.Name == name); 27 | } 28 | 29 | public static Expression> AndTypeEqualTo( 30 | this Expression> predicate, string type) 31 | { 32 | if (string.IsNullOrEmpty(type)) 33 | { 34 | return predicate; 35 | } 36 | return predicate.And(equipment => equipment.Type == type); 37 | } 38 | 39 | public static Expression> AndStatusIsNotRetired( 40 | this Expression> predicate) 41 | { 42 | return predicate.And(equipment => equipment.Status != EquipmentStatus.Retired); 43 | } 44 | 45 | public static Expression> AndSerialNumberEqualTo( 46 | this Expression> predicate, string serialNumber) 47 | { 48 | if (string.IsNullOrEmpty(serialNumber)) 49 | { 50 | return predicate; 51 | } 52 | return predicate.And(equipment => equipment.SerialNumber == serialNumber); 53 | } 54 | 55 | public static Expression> AndStatusEqualTo( 56 | this Expression> predicate, EquipmentStatus? status) 57 | { 58 | if (status == default) 59 | { 60 | return predicate; 61 | } 62 | return predicate.And(equipment => equipment.Status == status); 63 | } 64 | 65 | public static Expression> AndAssignmentDateEqualTo( 66 | this Expression> predicate, DateTime? assignmentDate) 67 | { 68 | if (!assignmentDate.HasValue) 69 | { 70 | return predicate; 71 | } 72 | return predicate.And(equipment => equipment.AssignmentDate == assignmentDate); 73 | } 74 | 75 | public static Expression> AndReturnDateEqualTo( 76 | this Expression> predicate, DateTime? returnDate) 77 | { 78 | if (!returnDate.HasValue) 79 | { 80 | return predicate; 81 | } 82 | return predicate.And(equipment => equipment.ReturnDate == returnDate); 83 | } 84 | 85 | public static Expression> AndAssignedToUserIdEqualTo( 86 | this Expression> predicate, Guid? assignedToUserId) 87 | { 88 | if (!assignedToUserId.HasValue) 89 | { 90 | return predicate; 91 | } 92 | return predicate.And(equipment => equipment.AssignedToUserId == assignedToUserId); 93 | } 94 | } -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipment/GetEquipmentQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.Dtos.Equipments; 3 | 4 | namespace TechTrack.Application.Equipments.Queries.GetEquipment 5 | { 6 | public class GetEquipmentQuery : IRequest 7 | { 8 | public GetEquipmentQuery(Guid id) 9 | { 10 | Id = id; 11 | } 12 | 13 | public Guid Id { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipment/GetEquipmentQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using TechTrack.Common.Dtos.Equipments; 4 | using TechTrack.Application.Interfaces.Equipments; 5 | 6 | namespace TechTrack.Application.Equipments.Queries.GetEquipment 7 | { 8 | public class GetEquipmentQueryHandler : IRequestHandler 9 | { 10 | private IEquipmentReadRepository _equipmentRepository; 11 | private IMapper _mapper; 12 | 13 | public GetEquipmentQueryHandler(IEquipmentReadRepository equipmentRepository, IMapper mapper) 14 | { 15 | _equipmentRepository = equipmentRepository; 16 | _mapper = mapper; 17 | } 18 | 19 | public async Task Handle(GetEquipmentQuery request, CancellationToken cancellationToken) 20 | { 21 | var equipment = await _equipmentRepository.GetByIdAsync(request.Id, cancellationToken); 22 | 23 | return _mapper.Map(equipment); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipment/GetEquipmentQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TechTrack.Application.Equipments.Queries.GetEquipment 4 | { 5 | public class GetEquipmentQueryValidator : AbstractValidator 6 | { 7 | public GetEquipmentQueryValidator() 8 | { 9 | RuleFor(q => q.Id) 10 | .NotEmpty() 11 | .NotNull(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipments/GetEquipmentsQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.Pagination; 3 | using TechTrack.Common.ViewModel.Equipments; 4 | 5 | namespace TechTrack.Application.Equipments.Queries.GetEquipments 6 | { 7 | public class GetEquipmentsQuery : IRequest> 8 | { 9 | public GetEquipmentsQuery(EquipmentInputVm filter) 10 | { 11 | Filter = filter; 12 | } 13 | 14 | public EquipmentInputVm Filter { get; set; } 15 | 16 | public bool ShouldSortAscending() 17 | { 18 | var sortDirection = string.IsNullOrWhiteSpace(Filter?.SortDirection) 19 | ? "asc" 20 | : Filter.SortDirection; 21 | 22 | return sortDirection.Equals( 23 | "asc", 24 | StringComparison.OrdinalIgnoreCase); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipments/GetEquipmentsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using System.Linq.Expressions; 4 | using TechTrack.Common.Pagination; 5 | using TechTrack.Common.Dtos.Equipments; 6 | using TechTrack.Application.Equipments.Extensions; 7 | using TechTrack.Common.ViewModel.Equipments; 8 | using TechTrack.Application.Helpers; 9 | using TechTrack.Application.Interfaces.Equipments; 10 | using TechTrack.Domain.Models; 11 | using static TechTrack.Application.Constants.EquipmentSortingConstants; 12 | 13 | namespace TechTrack.Application.Equipments.Queries.GetEquipments 14 | { 15 | public class GetEquipmentsQueryHandler : IRequestHandler> 16 | { 17 | private readonly IEquipmentReadRepository _equipmentReadRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public GetEquipmentsQueryHandler(IEquipmentReadRepository equipmentReadRepository, IMapper mapper) 21 | { 22 | _equipmentReadRepository = equipmentReadRepository; 23 | _mapper = mapper; 24 | } 25 | 26 | public async Task> Handle(GetEquipmentsQuery request, CancellationToken cancellationToken) 27 | { 28 | var filter = new EquipmentFilterDto( 29 | request.Filter.PageNumber, 30 | request.Filter.PageSize) 31 | { 32 | SearchFilter = GetFilter(request), 33 | SortExpression = GetSortingExpression(request.Filter.SortBy), 34 | SortAscending = request.ShouldSortAscending() 35 | }; 36 | 37 | var paginatedEquipments = await GetEquipments(filter, cancellationToken); 38 | 39 | var equipmentsVm = paginatedEquipments.Data.Select(_mapper.Map) 40 | .ToList(); 41 | 42 | return new PaginatedResult( 43 | equipmentsVm, 44 | request.Filter.PageNumber, 45 | request.Filter.PageSize, 46 | paginatedEquipments.TotalRecords); 47 | } 48 | 49 | private async Task> GetEquipments(EquipmentFilterDto filter, CancellationToken cancellationToken) 50 | { 51 | var paginatedEquipments = await _equipmentReadRepository.FilterEquipmentsAsync(filter, cancellationToken); 52 | 53 | if (paginatedEquipments.TotalRecords <= 0) 54 | { 55 | return paginatedEquipments; 56 | } 57 | 58 | return paginatedEquipments; 59 | } 60 | 61 | private static Expression> GetFilter(GetEquipmentsQuery filterOn) 62 | { 63 | return PredicateBuilder.True() 64 | .HasGuidEqualTo(filterOn.Filter.Id) 65 | .AndStatusIsNotRetired() 66 | .AndNameEqualTo(filterOn.Filter.Name) 67 | .AndTypeEqualTo(filterOn.Filter.Type) 68 | .AndSerialNumberEqualTo(filterOn.Filter.SerialNumber) 69 | .AndStatusEqualTo(filterOn.Filter.Status) 70 | .AndAssignmentDateEqualTo(filterOn.Filter.AssignmentDate) 71 | .AndReturnDateEqualTo(filterOn.Filter.ReturnDate) 72 | .AndAssignedToUserIdEqualTo(filterOn.Filter.AssignedToUserId); 73 | } 74 | 75 | private static Expression>? GetSortingExpression(string sortBy) 76 | { 77 | var defaultSortBy = SortExpressions[Name]; 78 | 79 | if (string.IsNullOrWhiteSpace(sortBy)) 80 | { 81 | return defaultSortBy; 82 | } 83 | 84 | if (SortExpressions.ContainsKey(sortBy)) 85 | { 86 | return SortExpressions[sortBy]; 87 | } 88 | 89 | return SortExpressions.ContainsKey(sortBy) 90 | ? null 91 | : defaultSortBy; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /TechTrack.Application/Equipments/Queries/GetEquipments/GetEquipmentsQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TechTrack.Application.Equipments.Queries.GetEquipments; 4 | public class GetEquipmentsQueryValidator : AbstractValidator 5 | { 6 | public GetEquipmentsQueryValidator() 7 | { 8 | RuleFor(query => query.Filter.PageSize) 9 | .InclusiveBetween(1, 100); 10 | 11 | RuleFor(query => query.Filter.PageNumber) 12 | .GreaterThan(0); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Application/EventHandlers/EquipmentCreatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Events; 3 | using TechTrack.Application.Interfaces.Equipments; 4 | 5 | namespace TechTrack.Application.EventHandlers 6 | { 7 | public class EquipmentCreatedEventHandler : 8 | INotificationHandler> 9 | { 10 | private readonly IEquipmentReadRepository _equipmentReadRepository; 11 | 12 | public EquipmentCreatedEventHandler(IEquipmentReadRepository equipmentReadRepository) 13 | { 14 | _equipmentReadRepository = equipmentReadRepository; 15 | } 16 | 17 | public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 18 | { 19 | _equipmentReadRepository.Add(notification.DomainEvent.Equipment); 20 | await _equipmentReadRepository.SaveChangesAsync(cancellationToken); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TechTrack.Application/EventHandlers/EquipmentRetiredEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Events; 3 | using TechTrack.Application.Interfaces.Equipments; 4 | 5 | namespace TechTrack.Application.EventHandlers 6 | { 7 | public class EquipmentRetiredEventHandler : 8 | INotificationHandler> 9 | { 10 | private readonly IEquipmentReadRepository _equipmentReadRepository; 11 | 12 | public EquipmentRetiredEventHandler(IEquipmentReadRepository equipmentReadRepository) 13 | { 14 | _equipmentReadRepository = equipmentReadRepository; 15 | } 16 | 17 | public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 18 | { 19 | _equipmentReadRepository.Retire(notification.DomainEvent.Id); 20 | await _equipmentReadRepository.SaveChangesAsync(cancellationToken); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /TechTrack.Application/EventHandlers/EquipmentUpdatedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Events; 3 | using TechTrack.Application.Interfaces.Equipments; 4 | 5 | namespace TechTrack.Application.EventHandlers 6 | { 7 | public class EquipmentUpdatedEventHandler : INotificationHandler> 8 | { 9 | private readonly IEquipmentReadRepository _equipmentReadRepository; 10 | 11 | public EquipmentUpdatedEventHandler(IEquipmentReadRepository equipmentReadRepository) 12 | { 13 | _equipmentReadRepository = equipmentReadRepository; 14 | } 15 | 16 | public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) 17 | { 18 | _equipmentReadRepository.Update(notification.DomainEvent.Equipment); 19 | await _equipmentReadRepository.SaveChangesAsync(cancellationToken); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TechTrack.Application/Events/DomainEventNotification.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Domain.Common; 3 | 4 | namespace TechTrack.Application.Events 5 | { 6 | public class DomainEventNotification : INotification 7 | where TDomainEvent : DomainEvent 8 | { 9 | public DomainEventNotification(TDomainEvent domainEvent) 10 | { 11 | DomainEvent = domainEvent; 12 | } 13 | 14 | public TDomainEvent DomainEvent { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TechTrack.Application/Events/DomainEventService.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Common.Interfaces; 3 | using TechTrack.Domain.Common; 4 | 5 | namespace TechTrack.Application.Events; 6 | public class DomainEventService : IDomainEventService 7 | { 8 | private readonly IMediator _mediator; 9 | 10 | public DomainEventService(IMediator mediator) 11 | { 12 | _mediator = mediator; 13 | } 14 | 15 | public async Task Raise(DomainEvent domainEvent, CancellationToken cancellationToken) 16 | { 17 | await _mediator.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent), cancellationToken); 18 | } 19 | 20 | private INotification GetNotificationCorrespondingToDomainEvent(DomainEvent domainEvent) 21 | { 22 | return (INotification)Activator.CreateInstance( 23 | typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()), domainEvent); 24 | } 25 | } -------------------------------------------------------------------------------- /TechTrack.Application/Events/EquipmentCreated.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Common; 2 | using TechTrack.Domain.Models; 3 | 4 | namespace TechTrack.Application.Events 5 | { 6 | public class EquipmentCreated : DomainEvent 7 | { 8 | public EquipmentCreated(Equipment equipment) 9 | { 10 | Equipment = equipment; 11 | } 12 | 13 | public Equipment Equipment { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TechTrack.Application/Events/EquipmentRetired.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Common; 2 | 3 | namespace TechTrack.Application.Events 4 | { 5 | public class EquipmentRetired : DomainEvent 6 | { 7 | public EquipmentRetired(Guid id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public Guid Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Application/Events/EquipmentUpdated.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Common; 2 | using TechTrack.Domain.Models; 3 | 4 | namespace TechTrack.Application.Events 5 | { 6 | public class EquipmentUpdated : DomainEvent 7 | { 8 | public EquipmentUpdated(Equipment equipment) 9 | { 10 | Equipment = equipment; 11 | } 12 | 13 | public Equipment Equipment { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TechTrack.Application/Helpers/PredicateBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace TechTrack.Application.Helpers 4 | { 5 | public static class PredicateBuilder 6 | { 7 | public static Expression> True() 8 | { 9 | return f => true; 10 | } 11 | 12 | public static Expression> And(this Expression> expression1, Expression> expression2) 13 | { 14 | var invokedExpression = Expression.Invoke(expression2, expression1.Parameters); 15 | 16 | return Expression.Lambda>(Expression.AndAlso(expression1.Body, invokedExpression), expression1.Parameters); 17 | } 18 | 19 | public static Expression> Or(this Expression> expr1, Expression> expr2) 20 | { 21 | var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); 22 | 23 | return Expression.Lambda>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Equipments/IEquipmentReadRepository.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.Pagination; 2 | using TechTrack.Common.Dtos.Equipments; 3 | using TechTrack.Domain.Models; 4 | 5 | namespace TechTrack.Application.Interfaces.Equipments 6 | { 7 | public interface IEquipmentReadRepository 8 | { 9 | Task> FilterEquipmentsAsync(EquipmentFilterDto filter, 10 | CancellationToken cancellationToken); 11 | Task GetByIdAsync(Guid id, CancellationToken cancellationToken); 12 | void Add(Equipment equipment); 13 | void Update(Equipment equipment); 14 | void Retire(Guid id); 15 | Task SaveChangesAsync(CancellationToken cancellationToken); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Equipments/IEquipmentWriteRepository.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Models; 2 | 3 | namespace TechTrack.Application.Interfaces.Equipments 4 | { 5 | public interface IEquipmentWriteRepository 6 | { 7 | Task GetEquipmentAsync(Guid id, CancellationToken cancellationToken); 8 | void Add(Equipment equipment); 9 | void Update(Equipment equipment); 10 | void Retire(Guid id); 11 | Task SaveChangesAsync(CancellationToken cancellationToken); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Equipments/IEquipmentsHttpClientService.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.Pagination; 2 | using TechTrack.Common.Dtos.Equipments; 3 | using TechTrack.Common.ViewModel.Equipments; 4 | 5 | namespace TechTrack.Application.Interfaces.Equipments 6 | { 7 | public interface IEquipmentsHttpClientService 8 | { 9 | Task> GetEquipmentsAsync(EquipmentInputVm equipmentsFilter); 10 | Task GetEquipmentAsync(Guid id); 11 | Task CreateEquipmentAsync(EquipmentForCreationDto equipmentDto); 12 | Task UpdateEquipmentAsync(Guid id, EquipmentForUpdateDto equipmentDto); 13 | Task RetireEquipmentAsync(Guid id); 14 | } 15 | } -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Users/IUserHttpClientService.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.ViewModel.Users; 2 | 3 | namespace TechTrack.Application.Interfaces.Users; 4 | public interface IUserHttpClientService 5 | { 6 | Task> GetUsersWithEquipmentsAsync(); 7 | } 8 | -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Users/IUsersReadRepository.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.ViewModel.Users; 2 | 3 | namespace TechTrack.Application.Interfaces.Users 4 | { 5 | public interface IUsersReadRepository 6 | { 7 | 8 | Task> GetUsersWithEquipmentsAsync(); 9 | Task GetAssignedUserName(Guid id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TechTrack.Application/Interfaces/Users/IUsersWriteRepository.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Models; 2 | 3 | namespace TechTrack.Application.Interfaces.Users; 4 | public interface IUsersWriteRepository 5 | { 6 | void Add(User user); 7 | } 8 | -------------------------------------------------------------------------------- /TechTrack.Application/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using TechTrack.Common.Dtos.Equipments; 3 | using TechTrack.Common.ViewModel.Equipments; 4 | using TechTrack.Domain.Models; 5 | 6 | namespace TechTrack.Application 7 | { 8 | public class MappingProfile : Profile 9 | { 10 | public MappingProfile() 11 | { 12 | CreateMap().ReverseMap(); 13 | CreateMap().ReverseMap(); 14 | CreateMap().ReverseMap(); 15 | CreateMap(); 16 | CreateMap() 17 | .ForMember(dest => dest.AssignedToUserName, opt => opt.MapFrom(src => src.AssignedTo.Name)); ; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TechTrack.Application/TechTrack.Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TechTrack.Application/Users/Commands/CreateUserCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.Dtos.Users; 3 | 4 | namespace TechTrack.Application.Users.Commands; 5 | public class CreateUserCommand : IRequest 6 | { 7 | public UserForCreationDto User { get; set; } 8 | 9 | public CreateUserCommand(UserForCreationDto user) 10 | { 11 | User = user; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TechTrack.Application/Users/Commands/CreateUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using TechTrack.Application.Interfaces.Users; 4 | using TechTrack.Domain.Models; 5 | 6 | namespace TechTrack.Application.Users.Commands; 7 | public class CreateUserCommandHandler : IRequestHandler 8 | { 9 | private readonly IUsersWriteRepository _userRepository; 10 | private readonly IMapper _mapper; 11 | 12 | public CreateUserCommandHandler(IUsersWriteRepository userRepository, IMapper mapper) 13 | { 14 | _userRepository = userRepository; 15 | _mapper = mapper; 16 | } 17 | 18 | public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) 19 | { 20 | var user = _mapper.Map(request.User); 21 | 22 | _userRepository.Add(user); 23 | 24 | return user.Id; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /TechTrack.Application/Users/Queries/GetUserWithEquipmentQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Common.ViewModel.Users; 3 | 4 | namespace TechTrack.Application.Users.Queries; 5 | public class GetUsersWithEquipmentsQuery : IRequest> 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /TechTrack.Application/Users/Queries/GetUsersWithEquipmentsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using TechTrack.Application.Interfaces.Users; 3 | using TechTrack.Common.ViewModel.Users; 4 | 5 | namespace TechTrack.Application.Users.Queries; 6 | 7 | public class GetUsersWithEquipmentsQueryHandler : IRequestHandler> 8 | { 9 | private readonly IUsersReadRepository _userReadRepository; 10 | 11 | public GetUsersWithEquipmentsQueryHandler(IUsersReadRepository userReadRepository) 12 | { 13 | _userReadRepository = userReadRepository; 14 | } 15 | 16 | public async Task> Handle(GetUsersWithEquipmentsQuery request, CancellationToken cancellationToken) 17 | { 18 | return await _userReadRepository.GetUsersWithEquipmentsAsync(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TechTrack.Common/Dtos/Equipments/EquipmentDto.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Enums; 2 | 3 | namespace TechTrack.Common.Dtos.Equipments 4 | { 5 | public class EquipmentDto 6 | { 7 | public Guid Id { get; set; } 8 | public string? Name { get; set; } 9 | public string? Type { get; set; } 10 | public string? SerialNumber { get; set; } 11 | public EquipmentStatus Status { get; set; } 12 | public DateTime? AssignmentDate { get; set; } 13 | public DateTime? ReturnDate { get; set; } 14 | public Guid? AssignedToUserId { get; set; } 15 | public string? AssignedToUserName { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /TechTrack.Common/Dtos/Equipments/EquipmentFilterDto.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using TechTrack.Common.Pagination; 3 | using TechTrack.Domain.Models; 4 | 5 | namespace TechTrack.Common.Dtos.Equipments 6 | { 7 | public class EquipmentFilterDto : PaginationFilter 8 | { 9 | public EquipmentFilterDto(int pageNumber, int pageSize) 10 | { 11 | PageNumber = pageNumber; 12 | PageSize = pageSize; 13 | } 14 | 15 | public Expression> SearchFilter { get; set; } 16 | 17 | public Expression> SortExpression { get; set; } 18 | 19 | public bool SortAscending { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TechTrack.Common/Dtos/Equipments/EquipmentForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using TechTrack.Domain.Enums; 3 | 4 | namespace TechTrack.Common.Dtos.Equipments 5 | { 6 | public class EquipmentForCreationDto 7 | { 8 | [Required(ErrorMessage = "Name is required")] 9 | [StringLength(50, ErrorMessage = "Name must be between 1 and 50 characters", MinimumLength = 1)] 10 | public string? Name { get; set; } 11 | [Required(ErrorMessage = "Type is required")] 12 | [StringLength(50, ErrorMessage = "Type must be between 1 and 50 characters", MinimumLength = 1)] 13 | public string? Type { get; set; } 14 | public string? SerialNumber { get; set; } 15 | public EquipmentStatus Status { get; set; } 16 | public DateTime? AssignmentDate { get; set; } 17 | public DateTime? ReturnDate { get; set; } 18 | public Guid? AssignedToUserId { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TechTrack.Common/Dtos/Equipments/EquipmentForUpdateDto.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Enums; 2 | 3 | namespace TechTrack.Common.Dtos.Equipments 4 | { 5 | public class EquipmentForUpdateDto 6 | { 7 | public Guid Id { get; set; } 8 | public string? Name { get; set; } 9 | public string? Type { get; set; } 10 | public string? SerialNumber { get; set; } 11 | public EquipmentStatus Status { get; set; } 12 | public DateTime? AssignmentDate { get; set; } 13 | public DateTime? ReturnDate { get; set; } 14 | public Guid? AssignedToUserId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TechTrack.Common/Dtos/Users/UserForCreationDto.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Common.Dtos.Users; 2 | public class UserForCreationDto 3 | { 4 | public string Name { get; set; } = string.Empty; 5 | public string Email { get; set; } = string.Empty; 6 | public string Department { get; set; } = string.Empty; 7 | } 8 | -------------------------------------------------------------------------------- /TechTrack.Common/Interfaces/HttpClients/IEquipmentsHttpClientService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using TechTrack.Common.Pagination; 3 | using TechTrack.Common.Dtos.Equipments; 4 | using TechTrack.Common.ViewModel.Equipments; 5 | 6 | namespace TechTrack.Common.Interfaces.HttpClients 7 | { 8 | public interface IEquipmentsHttpClientService 9 | { 10 | Task> GetEquipmentsAsync( 11 | EquipmentInputVm equipmentsFilter); 12 | Task GetEquipmentAsync(Guid id); 13 | Task CreateEquipmentAsync(EquipmentForCreationDto equipmentDto); 14 | Task UpdateEquipmentAsync(Guid id, EquipmentForUpdateDto equipmentDto); 15 | Task RetireEquipmentAsync(Guid id); 16 | } 17 | } -------------------------------------------------------------------------------- /TechTrack.Common/Interfaces/HttpClients/IUserHttpClientService.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.Dtos.Users; 2 | using TechTrack.Common.ViewModel.Users; 3 | 4 | namespace TechTrack.Common.Interfaces.HttpClients 5 | { 6 | public interface IUserHttpClientService 7 | { 8 | Task> GetUsersWithEquipmentsAsync(); 9 | Task AddUserAsync(UserForCreationDto userForCreationDto); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TechTrack.Common/Interfaces/IDomainEventService.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Common; 2 | 3 | namespace TechTrack.Application.Common.Interfaces 4 | { 5 | public interface IDomainEventService 6 | { 7 | Task Raise(DomainEvent domainEvent, CancellationToken cancellationToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TechTrack.Common/Pagination/PaginatedResult.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Common.Pagination 2 | { 3 | public class PaginatedResult 4 | { 5 | public PaginatedResult(IList data, int pageNumber, int pageSize, int totalRecords) 6 | { 7 | Data = data; 8 | PageNumber = pageNumber; 9 | PageSize = pageSize; 10 | TotalRecords = totalRecords; 11 | 12 | if (pageSize > 0 && totalRecords > 0) 13 | { 14 | TotalPages = Convert.ToInt32(Math.Ceiling((double)totalRecords / pageSize)); 15 | } 16 | } 17 | 18 | public IList Data { get; } 19 | 20 | public int PageNumber { get; } 21 | 22 | public int PageSize { get; } 23 | 24 | public int TotalRecords { get; } 25 | 26 | public int TotalPages { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TechTrack.Common/Pagination/PaginationFilter.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Common.Pagination 2 | { 3 | public class PaginationFilter 4 | { 5 | public int PageNumber { get; set; } = 1; 6 | 7 | public int PageSize { get; set; } = 10; 8 | 9 | public int GetSkipCount() 10 | { 11 | return (PageNumber - 1) * PageSize; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TechTrack.Common/TechTrack.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /TechTrack.Common/ViewModels/Equipments/EquipmentInputVm.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.Pagination; 2 | using TechTrack.Domain.Enums; 3 | 4 | namespace TechTrack.Common.ViewModel.Equipments 5 | { 6 | public class EquipmentInputVm : PaginationFilter 7 | { 8 | public Guid Id { get; set; } 9 | public string? Name { get; set; } 10 | public string? Type { get; set; } 11 | public string? SerialNumber { get; set; } 12 | public EquipmentStatus? Status { get; set; } 13 | public DateTime? AssignmentDate { get; set; } 14 | public DateTime? ReturnDate { get; set; } 15 | public Guid? AssignedToUserId { get; set; } 16 | public string? SortBy { get; set; } 17 | public string? SortDirection { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /TechTrack.Common/ViewModels/Equipments/EquipmentOutputVm.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Enums; 2 | 3 | namespace TechTrack.Common.ViewModel.Equipments 4 | { 5 | public class EquipmentOutputVm 6 | { 7 | public Guid Id { get; set; } 8 | public string? Name { get; set; } 9 | public string? Type { get; set; } 10 | public string? SerialNumber { get; set; } 11 | public EquipmentStatus Status { get; set; } 12 | public DateTime? AssignmentDate { get; set; } 13 | public DateTime? ReturnDate { get; set; } 14 | public Guid? AssignedToUserId { get; set; } 15 | public string? AssignedToUserName { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /TechTrack.Common/ViewModels/Users/UserWithEquipmentVm.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Common.ViewModel.Equipments; 2 | 3 | namespace TechTrack.Common.ViewModel.Users 4 | { 5 | public class UserWithEquipmentsVm 6 | { 7 | public Guid UserId { get; set; } 8 | public string Name { get; set; } 9 | public string Email { get; set; } 10 | public string Department { get; set; } 11 | public List Equipments { get; set; } = new List(); 12 | } 13 | } -------------------------------------------------------------------------------- /TechTrack.Domain/Common/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Domain.Common 2 | { 3 | public abstract class DomainEvent 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TechTrack.Domain/Common/Entity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using TechTrack.Domain.Common.Interfaces; 3 | 4 | namespace TechTrack.Domain.Common 5 | { 6 | public class Entity : IHasDomainEvent 7 | { 8 | int? _requestedHashCode; 9 | int _Id; 10 | public virtual int Id 11 | { 12 | get => _Id; 13 | set => _Id = value; 14 | } 15 | 16 | [NotMapped] 17 | public List DomainEvents { get; set; } = new(); 18 | 19 | 20 | public bool IsTransient() 21 | { 22 | return this.Id == default(int); 23 | } 24 | 25 | public override bool Equals(object obj) 26 | { 27 | if (obj == null || !(obj is Entity)) 28 | return false; 29 | 30 | if (object.ReferenceEquals(this, obj)) 31 | return true; 32 | 33 | if (this.GetType() != obj.GetType()) 34 | return false; 35 | 36 | Entity item = (Entity)obj; 37 | 38 | if (item.IsTransient() || this.IsTransient()) 39 | return false; 40 | else 41 | return item.Id == this.Id; 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | if (!IsTransient()) 47 | { 48 | if (!_requestedHashCode.HasValue) 49 | _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) 50 | 51 | return _requestedHashCode.Value; 52 | } 53 | else 54 | return base.GetHashCode(); 55 | 56 | } 57 | public static bool operator ==(Entity left, Entity right) 58 | { 59 | if (Equals(left, null)) 60 | return (Equals(right, null)) ? true : false; 61 | else 62 | return left.Equals(right); 63 | } 64 | 65 | public static bool operator !=(Entity left, Entity right) 66 | { 67 | return !(left == right); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /TechTrack.Domain/Common/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | public static class EnumExtensions 4 | { 5 | public static string GetDescription(this Enum value) 6 | { 7 | var field = value.GetType().GetField(value.ToString()); 8 | var attribute = (DescriptionAttribute)Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)); 9 | return attribute?.Description ?? value.ToString(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TechTrack.Domain/Common/Interfaces/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Domain.Common.Interfaces 2 | { 3 | public interface IAggregateRoot 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TechTrack.Domain/Common/Interfaces/IHasDomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Domain.Common.Interfaces 2 | { 3 | public interface IHasDomainEvent 4 | { 5 | List DomainEvents { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TechTrack.Domain/Common/ValueObject.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Domain.Common 2 | { 3 | public abstract class ValueObject 4 | { 5 | protected static bool EqualOperator(ValueObject left, ValueObject right) 6 | { 7 | if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) 8 | { 9 | return false; 10 | } 11 | 12 | return ReferenceEquals(left, null) || left.Equals(right); 13 | } 14 | 15 | protected abstract IEnumerable GetEqualityComponents(); 16 | public override bool Equals(object obj) 17 | { 18 | if (obj == null || obj.GetType() != GetType()) 19 | { 20 | return false; 21 | } 22 | 23 | var other = obj as ValueObject; 24 | 25 | return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return GetHashCodeCore(); 31 | } 32 | 33 | protected abstract int GetHashCodeCore(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TechTrack.Domain/Enums/EquipmentStatus.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace TechTrack.Domain.Enums 4 | { 5 | public enum EquipmentStatus 6 | { 7 | [Description("Available")] 8 | Available, 9 | [Description("Assigned")] 10 | Assigned, 11 | [Description("Under Maintenance")] 12 | UnderMaintenance, 13 | [Description("Retired")] 14 | Retired 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TechTrack.Domain/Models/Equipment.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Domain.Common; 2 | using TechTrack.Domain.Common.Interfaces; 3 | using TechTrack.Domain.Enums; 4 | 5 | namespace TechTrack.Domain.Models 6 | { 7 | public class Equipment : IHasDomainEvent 8 | { 9 | public Guid Id { get; set; } 10 | public string? Name { get; set; } 11 | public string? Type { get; set; } 12 | public string? SerialNumber { get; set; } 13 | public EquipmentStatus Status { get; set; } 14 | public DateTime? AssignmentDate { get; set; } 15 | public DateTime? ReturnDate { get; set; } 16 | public Guid? AssignedToUserId { get; set; } 17 | public User? AssignedTo { get; set; } 18 | public List DomainEvents { get; set; } = new List(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TechTrack.Domain/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace TechTrack.Domain.Models 2 | { 3 | public class User 4 | { 5 | public Guid Id { get; set; } 6 | public string Name { get; set; } = string.Empty; 7 | public string Email { get; set; } = string.Empty; 8 | public string Department { get; set; } = string.Empty; 9 | public ICollection AssignedEquipments { get; set; } = new List(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TechTrack.Domain/TechTrack.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TechTrack.Infrastructure/TechTrack.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Configurations/EquipmentConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using TechTrack.Domain.Enums; 4 | using TechTrack.Domain.Models; 5 | 6 | namespace TechTrack.Persistence.Configurations 7 | { 8 | public class EquipmentConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.HasKey(e => e.Id); 13 | 14 | builder.Property(e => e.Name) 15 | .IsRequired() 16 | .HasMaxLength(256); 17 | 18 | builder.Property(e => e.SerialNumber) 19 | .HasMaxLength(256); 20 | 21 | builder.Property(e => e.Type) 22 | .IsRequired() 23 | .HasMaxLength(128); 24 | 25 | builder.Property(e => e.Status) 26 | .HasMaxLength(50) 27 | .HasConversion( 28 | v => v.ToString(), 29 | v => (EquipmentStatus)Enum.Parse(typeof(EquipmentStatus), v)) 30 | .IsUnicode(false); 31 | 32 | builder.HasOne(e => e.AssignedTo) 33 | .WithMany(u => u.AssignedEquipments) 34 | .HasForeignKey(e => e.AssignedToUserId) 35 | .OnDelete(DeleteBehavior.SetNull); 36 | 37 | builder.Ignore(e => e.DomainEvents); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /TechTrack.Persistence/Configurations/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Microsoft.EntityFrameworkCore; 3 | using TechTrack.Domain.Models; 4 | 5 | namespace TechTrack.Persistence.Configurations 6 | { 7 | public class UserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(e => e.Id); 12 | 13 | builder.Property(e => e.Name) 14 | .IsRequired() 15 | .HasMaxLength(256); 16 | 17 | builder.Property(e => e.Email) 18 | .IsRequired() 19 | .HasMaxLength(256); 20 | 21 | builder.Property(e => e.Department) 22 | .IsRequired() 23 | .HasMaxLength(256); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /TechTrack.Persistence/DatabaseContext/ReadDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TechTrack.Domain.Models; 3 | using TechTrack.Persistence.Configurations; 4 | 5 | namespace TechTrack.Persistence.DatabaseContext 6 | { 7 | public class ReadDbContext : DbContext 8 | { 9 | public ReadDbContext() 10 | { 11 | 12 | } 13 | 14 | public ReadDbContext(DbContextOptions options) : base(options) 15 | { 16 | 17 | } 18 | 19 | public DbSet Equipments { get; set; } 20 | public DbSet Users { get; set; } 21 | 22 | protected override void OnModelCreating(ModelBuilder modelBuilder) 23 | { 24 | base.OnModelCreating(modelBuilder); 25 | 26 | modelBuilder.HasDefaultSchema("read"); 27 | modelBuilder.ApplyConfiguration(new EquipmentConfiguration()); 28 | modelBuilder.ApplyConfiguration(new UserConfiguration()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TechTrack.Persistence/DatabaseContext/WriteDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TechTrack.Application.Common.Interfaces; 3 | using TechTrack.Domain.Common; 4 | using TechTrack.Domain.Common.Interfaces; 5 | using TechTrack.Domain.Models; 6 | using TechTrack.Persistence.Configurations; 7 | 8 | namespace TechTrack.Persistence.DatabaseContext 9 | { 10 | public class WriteDbContext : DbContext 11 | { 12 | private readonly IDomainEventService _domainEventService; 13 | 14 | public WriteDbContext(DbContextOptions options, IDomainEventService domainEventService) : base(options) 15 | { 16 | _domainEventService = domainEventService; 17 | } 18 | 19 | public DbSet Equipments { get; set; } 20 | public DbSet Users { get; set; } 21 | 22 | protected override void OnModelCreating(ModelBuilder modelBuilder) 23 | { 24 | base.OnModelCreating(modelBuilder); 25 | 26 | modelBuilder.HasDefaultSchema("write"); 27 | modelBuilder.ApplyConfiguration(new EquipmentConfiguration()); 28 | } 29 | 30 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) 31 | { 32 | var result = await base.SaveChangesAsync(cancellationToken); 33 | 34 | await DispatchEvents(cancellationToken); 35 | 36 | return result; 37 | } 38 | 39 | private async Task DispatchEvents(CancellationToken cancellationToken) 40 | { 41 | var domainEventEntities = ChangeTracker.Entries() 42 | .SelectMany(entity => entity.Entity.DomainEvents) 43 | .ToArray(); 44 | 45 | foreach (var domainEvent in domainEventEntities) 46 | { 47 | await _domainEventService.Raise(domainEvent, cancellationToken); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Migrations/Read/20240419195506_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace TechTrack.Persistence.Migrations.Read 7 | { 8 | /// 9 | public partial class InitialMigration : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.EnsureSchema( 15 | name: "read"); 16 | 17 | migrationBuilder.CreateTable( 18 | name: "Users", 19 | schema: "read", 20 | columns: table => new 21 | { 22 | Id = table.Column(type: "uniqueidentifier", nullable: false), 23 | Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 24 | Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 25 | Department = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_Users", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.CreateTable( 33 | name: "Equipments", 34 | schema: "read", 35 | columns: table => new 36 | { 37 | Id = table.Column(type: "uniqueidentifier", nullable: false), 38 | Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 39 | Type = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 40 | SerialNumber = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 41 | Status = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), 42 | AssignmentDate = table.Column(type: "datetime2", nullable: true), 43 | ReturnDate = table.Column(type: "datetime2", nullable: true), 44 | AssignedToUserId = table.Column(type: "uniqueidentifier", nullable: true) 45 | }, 46 | constraints: table => 47 | { 48 | table.PrimaryKey("PK_Equipments", x => x.Id); 49 | table.ForeignKey( 50 | name: "FK_Equipments_Users_AssignedToUserId", 51 | column: x => x.AssignedToUserId, 52 | principalSchema: "read", 53 | principalTable: "Users", 54 | principalColumn: "Id", 55 | onDelete: ReferentialAction.SetNull); 56 | }); 57 | 58 | migrationBuilder.CreateIndex( 59 | name: "IX_Equipments_AssignedToUserId", 60 | schema: "read", 61 | table: "Equipments", 62 | column: "AssignedToUserId"); 63 | } 64 | 65 | /// 66 | protected override void Down(MigrationBuilder migrationBuilder) 67 | { 68 | migrationBuilder.DropTable( 69 | name: "Equipments", 70 | schema: "read"); 71 | 72 | migrationBuilder.DropTable( 73 | name: "Users", 74 | schema: "read"); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Migrations/Read/20240426191153_UpdatedSerialNumber.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace TechTrack.Persistence.Migrations.Read 6 | { 7 | /// 8 | public partial class UpdatedSerialNumber : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AlterColumn( 14 | name: "SerialNumber", 15 | schema: "read", 16 | table: "Equipments", 17 | type: "nvarchar(256)", 18 | maxLength: 256, 19 | nullable: true, 20 | oldClrType: typeof(string), 21 | oldType: "nvarchar(256)", 22 | oldMaxLength: 256); 23 | } 24 | 25 | /// 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.AlterColumn( 29 | name: "SerialNumber", 30 | schema: "read", 31 | table: "Equipments", 32 | type: "nvarchar(256)", 33 | maxLength: 256, 34 | nullable: false, 35 | defaultValue: "", 36 | oldClrType: typeof(string), 37 | oldType: "nvarchar(256)", 38 | oldMaxLength: 256, 39 | oldNullable: true); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Migrations/Write/20240419195535_InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace TechTrack.Persistence.Migrations.Write 7 | { 8 | /// 9 | public partial class InitialMigration : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.EnsureSchema( 15 | name: "write"); 16 | 17 | migrationBuilder.CreateTable( 18 | name: "Users", 19 | schema: "write", 20 | columns: table => new 21 | { 22 | Id = table.Column(type: "uniqueidentifier", nullable: false), 23 | Name = table.Column(type: "nvarchar(max)", nullable: false), 24 | Email = table.Column(type: "nvarchar(max)", nullable: false), 25 | Department = table.Column(type: "nvarchar(max)", nullable: false) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_Users", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.CreateTable( 33 | name: "Equipments", 34 | schema: "write", 35 | columns: table => new 36 | { 37 | Id = table.Column(type: "uniqueidentifier", nullable: false), 38 | Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 39 | Type = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 40 | SerialNumber = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), 41 | Status = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), 42 | AssignmentDate = table.Column(type: "datetime2", nullable: true), 43 | ReturnDate = table.Column(type: "datetime2", nullable: true), 44 | AssignedToUserId = table.Column(type: "uniqueidentifier", nullable: true) 45 | }, 46 | constraints: table => 47 | { 48 | table.PrimaryKey("PK_Equipments", x => x.Id); 49 | table.ForeignKey( 50 | name: "FK_Equipments_Users_AssignedToUserId", 51 | column: x => x.AssignedToUserId, 52 | principalSchema: "write", 53 | principalTable: "Users", 54 | principalColumn: "Id", 55 | onDelete: ReferentialAction.SetNull); 56 | }); 57 | 58 | migrationBuilder.CreateIndex( 59 | name: "IX_Equipments_AssignedToUserId", 60 | schema: "write", 61 | table: "Equipments", 62 | column: "AssignedToUserId"); 63 | } 64 | 65 | /// 66 | protected override void Down(MigrationBuilder migrationBuilder) 67 | { 68 | migrationBuilder.DropTable( 69 | name: "Equipments", 70 | schema: "write"); 71 | 72 | migrationBuilder.DropTable( 73 | name: "Users", 74 | schema: "write"); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Migrations/Write/20240426191227_UpdatedSerialNumber.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace TechTrack.Persistence.Migrations.Write 6 | { 7 | /// 8 | public partial class UpdatedSerialNumber : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AlterColumn( 14 | name: "SerialNumber", 15 | schema: "write", 16 | table: "Equipments", 17 | type: "nvarchar(256)", 18 | maxLength: 256, 19 | nullable: true, 20 | oldClrType: typeof(string), 21 | oldType: "nvarchar(256)", 22 | oldMaxLength: 256); 23 | } 24 | 25 | /// 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.AlterColumn( 29 | name: "SerialNumber", 30 | schema: "write", 31 | table: "Equipments", 32 | type: "nvarchar(256)", 33 | maxLength: 256, 34 | nullable: false, 35 | defaultValue: "", 36 | oldClrType: typeof(string), 37 | oldType: "nvarchar(256)", 38 | oldMaxLength: 256, 39 | oldNullable: true); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Migrations/Write/WriteDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using TechTrack.Persistence.DatabaseContext; 8 | 9 | #nullable disable 10 | 11 | namespace TechTrack.Persistence.Migrations.Write 12 | { 13 | [DbContext(typeof(WriteDbContext))] 14 | partial class WriteDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasDefaultSchema("write") 21 | .HasAnnotation("ProductVersion", "8.0.3") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 23 | 24 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 25 | 26 | modelBuilder.Entity("TechTrack.Domain.Models.Equipment", b => 27 | { 28 | b.Property("Id") 29 | .ValueGeneratedOnAdd() 30 | .HasColumnType("uniqueidentifier"); 31 | 32 | b.Property("AssignedToUserId") 33 | .HasColumnType("uniqueidentifier"); 34 | 35 | b.Property("AssignmentDate") 36 | .HasColumnType("datetime2"); 37 | 38 | b.Property("Name") 39 | .IsRequired() 40 | .HasMaxLength(256) 41 | .HasColumnType("nvarchar(256)"); 42 | 43 | b.Property("ReturnDate") 44 | .HasColumnType("datetime2"); 45 | 46 | b.Property("SerialNumber") 47 | .HasMaxLength(256) 48 | .HasColumnType("nvarchar(256)"); 49 | 50 | b.Property("Status") 51 | .IsRequired() 52 | .HasMaxLength(50) 53 | .IsUnicode(false) 54 | .HasColumnType("varchar(50)"); 55 | 56 | b.Property("Type") 57 | .IsRequired() 58 | .HasMaxLength(128) 59 | .HasColumnType("nvarchar(128)"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("AssignedToUserId"); 64 | 65 | b.ToTable("Equipments", "write"); 66 | }); 67 | 68 | modelBuilder.Entity("TechTrack.Domain.Models.User", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd() 72 | .HasColumnType("uniqueidentifier"); 73 | 74 | b.Property("Department") 75 | .IsRequired() 76 | .HasColumnType("nvarchar(max)"); 77 | 78 | b.Property("Email") 79 | .IsRequired() 80 | .HasColumnType("nvarchar(max)"); 81 | 82 | b.Property("Name") 83 | .IsRequired() 84 | .HasColumnType("nvarchar(max)"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("Users", "write"); 89 | }); 90 | 91 | modelBuilder.Entity("TechTrack.Domain.Models.Equipment", b => 92 | { 93 | b.HasOne("TechTrack.Domain.Models.User", "AssignedTo") 94 | .WithMany("AssignedEquipments") 95 | .HasForeignKey("AssignedToUserId") 96 | .OnDelete(DeleteBehavior.SetNull); 97 | 98 | b.Navigation("AssignedTo"); 99 | }); 100 | 101 | modelBuilder.Entity("TechTrack.Domain.Models.User", b => 102 | { 103 | b.Navigation("AssignedEquipments"); 104 | }); 105 | #pragma warning restore 612, 618 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Repositories/EquipmentReadRepository.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.EntityFrameworkCore; 3 | using TechTrack.Common.Pagination; 4 | using TechTrack.Common.Dtos.Equipments; 5 | using TechTrack.Application.Interfaces.Equipments; 6 | using TechTrack.Domain.Models; 7 | using TechTrack.Persistence.DatabaseContext; 8 | using System.Linq.Dynamic.Core; 9 | using TechTrack.Domain.Enums; 10 | 11 | namespace TechTrack.Persistence.Repositories 12 | { 13 | public class EquipmentReadRepository : IEquipmentReadRepository 14 | { 15 | private readonly ReadDbContext _context; 16 | private readonly IMapper _mapper; 17 | 18 | public EquipmentReadRepository(ReadDbContext context, IMapper mapper) 19 | { 20 | _context = context; 21 | _mapper = mapper; 22 | } 23 | 24 | public async Task> FilterEquipmentsAsync(EquipmentFilterDto filter, CancellationToken cancellationToken) 25 | { 26 | var searchQuery = _context.Equipments 27 | .Include(e => e.AssignedTo) 28 | .AsQueryable() 29 | .OrderBy(filter.SortExpression) 30 | .AsNoTracking() 31 | .Where(filter.SearchFilter); 32 | 33 | var query = _mapper.ProjectTo(searchQuery); 34 | 35 | var totalRecords = await query.CountAsync(cancellationToken); 36 | 37 | if(totalRecords <= 0) 38 | { 39 | return new PaginatedResult( 40 | new List(), 41 | filter.PageNumber, 42 | filter.PageSize, 43 | totalRecords); 44 | } 45 | 46 | var equipments = await query 47 | .AsQueryable() 48 | .Skip(filter.GetSkipCount()) 49 | .Take(filter.PageSize) 50 | .ToListAsync(cancellationToken); 51 | 52 | return new PaginatedResult( 53 | equipments, 54 | filter.PageNumber, 55 | filter.PageSize, 56 | totalRecords); 57 | } 58 | 59 | public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) 60 | { 61 | var equipment = await _context.Equipments.FirstOrDefaultAsync(e => e.Id == id, cancellationToken); 62 | 63 | return equipment; 64 | } 65 | 66 | public void Add(Equipment equipment) 67 | { 68 | _context.Equipments.Add(equipment); 69 | } 70 | 71 | public void Retire(Guid id) 72 | { 73 | var equipment = _context.Equipments.Find(id); 74 | equipment.Status = EquipmentStatus.Retired; 75 | } 76 | 77 | public void Update(Equipment equipment) 78 | { 79 | _context.Equipments.Update(equipment); 80 | } 81 | 82 | public async Task SaveChangesAsync(CancellationToken cancellationToken) 83 | { 84 | await _context.SaveChangesAsync(cancellationToken); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Repositories/EquipmentWriteRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TechTrack.Application.Interfaces.Equipments; 3 | using TechTrack.Domain.Enums; 4 | using TechTrack.Domain.Models; 5 | using TechTrack.Persistence.DatabaseContext; 6 | 7 | namespace TechTrack.Persistence.Repositories 8 | { 9 | public class EquipmentWriteRepository : IEquipmentWriteRepository 10 | { 11 | private readonly WriteDbContext _context; 12 | 13 | public EquipmentWriteRepository(WriteDbContext context) 14 | { 15 | _context = context; 16 | } 17 | 18 | public void Add(Equipment equipment) 19 | { 20 | _context.Equipments.Add(equipment); 21 | } 22 | 23 | public void Retire(Guid id) 24 | { 25 | var equipment = _context.Equipments.Find(id); 26 | equipment.Status = EquipmentStatus.Retired; 27 | } 28 | 29 | public Task GetEquipmentAsync(Guid id, CancellationToken cancellationToken) 30 | { 31 | var equipment = _context.Equipments.FirstOrDefaultAsync(equipment => equipment.Id == id, cancellationToken); 32 | 33 | return equipment; 34 | } 35 | 36 | public async Task SaveChangesAsync(CancellationToken cancellationToken) 37 | { 38 | await _context.SaveChangesAsync(cancellationToken); 39 | } 40 | 41 | public void Update(Equipment equipment) 42 | { 43 | _context.Equipments.Update(equipment); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TechTrack.Persistence/Repositories/UsersReadRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TechTrack.Common.ViewModel.Equipments; 3 | using TechTrack.Application.Interfaces.Users; 4 | using TechTrack.Common.ViewModel.Users; 5 | using TechTrack.Domain.Models; 6 | using TechTrack.Persistence.DatabaseContext; 7 | 8 | namespace TechTrack.Persistence.Repositories 9 | { 10 | public class UsersReadRepository : IUsersReadRepository 11 | { 12 | private readonly ReadDbContext _context; 13 | 14 | public UsersReadRepository(ReadDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public async Task> GetUsersWithEquipmentsAsync() 20 | { 21 | var usersWithEquipments = await _context.Users 22 | .Select(u => new UserWithEquipmentsVm 23 | { 24 | UserId = u.Id, 25 | Name = u.Name, 26 | Email = u.Email, 27 | Department = u.Department, 28 | Equipments = u.AssignedEquipments.Select(eq => new EquipmentOutputVm 29 | { 30 | Id = eq.Id, 31 | Name = eq.Name, 32 | Type = eq.Type, 33 | SerialNumber = eq.SerialNumber, 34 | Status = eq.Status, 35 | AssignmentDate = eq.AssignmentDate, 36 | ReturnDate = eq.ReturnDate 37 | }).ToList() 38 | }).ToListAsync(); 39 | 40 | return usersWithEquipments; 41 | } 42 | public async Task GetAssignedUserName(Guid assignedToUserId) 43 | { 44 | var user = await _context.Users.FindAsync(assignedToUserId); 45 | return user?.Name; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /TechTrack.Persistence/Repositories/UsersWriteRepository.cs: -------------------------------------------------------------------------------- 1 | using TechTrack.Application.Interfaces.Users; 2 | using TechTrack.Domain.Models; 3 | using TechTrack.Persistence.DatabaseContext; 4 | 5 | namespace TechTrack.Persistence.Repositories; 6 | public class UsersWriteRepository : IUsersWriteRepository 7 | { 8 | private readonly WriteDbContext _dbContext; 9 | 10 | public UsersWriteRepository(WriteDbContext dbContext) 11 | { 12 | _dbContext = dbContext; 13 | } 14 | 15 | public void Add(User user) 16 | { 17 | _dbContext.Add(user); 18 | _dbContext.SaveChanges(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TechTrack.Persistence/TechTrack.Persistence.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/CreateEquipment/CreateEquipmentCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoMapper; 3 | using Moq; 4 | using TechTrack.Application; 5 | using TechTrack.Application.Equipments.Commands.CreateEquipment; 6 | using TechTrack.Common.Dtos.Equipments; 7 | using TechTrack.Application.Events; 8 | using TechTrack.Application.Interfaces.Equipments; 9 | using TechTrack.Domain.Models; 10 | 11 | namespace TechTrack.UnitTests.Application.Equipments.Commands.CreateEquipment 12 | { 13 | public class CreateEquipmentCommandHandlerTests 14 | { 15 | private readonly Mock _mockEquipmentRepository; 16 | private readonly IMapper _mapper; 17 | private readonly CreateEquipmentCommandHandler _handler; 18 | 19 | public CreateEquipmentCommandHandlerTests() 20 | { 21 | _mockEquipmentRepository = new Mock(); 22 | 23 | // Setup AutoMapper 24 | var configuration = new MapperConfiguration(cfg => 25 | { 26 | cfg.AddProfile(); 27 | }); 28 | _mapper = configuration.CreateMapper(); 29 | 30 | _handler = new CreateEquipmentCommandHandler(_mockEquipmentRepository.Object, _mapper); 31 | } 32 | 33 | [Fact] 34 | public async Task Handle_ValidRequest_AddsEquipmentToRepository() 35 | { 36 | // Arrange 37 | var fixture = new Fixture(); 38 | var equipmentForCreationDto = fixture.Create(); 39 | var request = new CreateEquipmentCommand (equipmentForCreationDto); 40 | 41 | _mockEquipmentRepository.Setup(repo => repo.SaveChangesAsync(It.IsAny())) 42 | .Returns(Task.CompletedTask); 43 | 44 | // Act 45 | await _handler.Handle(request, CancellationToken.None); 46 | 47 | // Assert 48 | _mockEquipmentRepository.Verify(repo => repo.Add(It.IsAny()), Times.Once); 49 | _mockEquipmentRepository.Verify(repo => repo.SaveChangesAsync(It.IsAny()), Times.Once); 50 | } 51 | 52 | [Fact] 53 | public async Task Handle_ValidRequest_TriggerEquipmentCreatedDomainEvent() 54 | { 55 | // Arrange 56 | var fixture = new Fixture(); 57 | var equipmentDto = fixture.Create(); 58 | var request = new CreateEquipmentCommand (equipmentDto); 59 | 60 | Equipment capturedEquipment = null; 61 | _mockEquipmentRepository.Setup(repo => repo.Add(It.IsAny())) 62 | .Callback(equipment => capturedEquipment = equipment); 63 | 64 | // Act 65 | await _handler.Handle(request, CancellationToken.None); 66 | 67 | // Assert 68 | Assert.NotNull(capturedEquipment); 69 | Assert.Contains(capturedEquipment.DomainEvents, e => e is EquipmentCreated); 70 | } 71 | 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/CreateEquipment/CreateEquipmentCommandValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.TestHelper; 2 | using TechTrack.Application.Equipments.Commands.CreateEquipment; 3 | using TechTrack.Common.Dtos.Equipments; 4 | 5 | namespace TechTrack.UnitTests.Application.Equipments.Commands.CreateEquipment 6 | { 7 | public class CreateEquipmentCommandValidatorTests 8 | { 9 | private readonly CreateEquipmentCommandValidator _validator; 10 | 11 | public CreateEquipmentCommandValidatorTests() 12 | { 13 | _validator = new CreateEquipmentCommandValidator(); 14 | } 15 | 16 | [Fact] 17 | public void AssignedToUserId_ShouldNotBeNull() 18 | { 19 | // Arrange 20 | var equipmentForCreationDto = new EquipmentForCreationDto { }; 21 | var model = new CreateEquipmentCommand(equipmentForCreationDto); 22 | 23 | // Act 24 | var result = _validator.TestValidate(model); 25 | 26 | // Assert 27 | result.ShouldHaveValidationErrorFor("EquipmentForCreationDto.AssignedToUserId"); 28 | } 29 | 30 | [Fact] 31 | public void ReturnDate_ShouldNotBeEmpty() 32 | { 33 | // Arrange 34 | var equipmentForCreationDto = new EquipmentForCreationDto { ReturnDate = null }; 35 | var model = new CreateEquipmentCommand(equipmentForCreationDto); 36 | 37 | // Act 38 | var result = _validator.TestValidate(model); 39 | 40 | // Assert 41 | result.ShouldHaveValidationErrorFor("EquipmentForCreationDto.ReturnDate"); 42 | } 43 | 44 | [Fact] 45 | public void Name_ShouldNotBeEmpty_AndNotExceedMaximumLength() 46 | { 47 | // Arrange 48 | var equipmentForCreationDto = new EquipmentForCreationDto { Name = string.Empty }; 49 | var model = new CreateEquipmentCommand(equipmentForCreationDto); 50 | 51 | // Act 52 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.Name"); 53 | 54 | // Assert 55 | model.EquipmentForCreationDto.Name = new string('A', 501); 56 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.Name"); 57 | } 58 | 59 | [Fact] 60 | public void SerialNumber_ShouldNotBeEmpty_AndNotExceedMaximumLength() 61 | { 62 | // Arrange 63 | var equipmentForCreationDto = new EquipmentForCreationDto { SerialNumber = string.Empty }; 64 | var model = new CreateEquipmentCommand(equipmentForCreationDto); 65 | 66 | // Act 67 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.SerialNumber"); 68 | 69 | // Assert 70 | model.EquipmentForCreationDto.SerialNumber = new string('A', 25); 71 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.SerialNumber"); 72 | } 73 | 74 | [Fact] 75 | public void Type_ShouldNotBeEmpty_AndNotExceedMaximumLength() 76 | { 77 | // Arrange 78 | var equipmentForCreationDto = new EquipmentForCreationDto { Type = string.Empty }; 79 | var model = new CreateEquipmentCommand( equipmentForCreationDto); 80 | 81 | // Act 82 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.Type"); 83 | 84 | // Assert 85 | model.EquipmentForCreationDto.Type = new string('A', 51); 86 | _validator.TestValidate(model).ShouldHaveValidationErrorFor("EquipmentForCreationDto.Type"); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/RetireEquipment/RetireEquipmentCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Moq; 3 | using TechTrack.Application.Equipments.Commands.DeleteEquipment; 4 | using TechTrack.Application.Events; 5 | using TechTrack.Application.Interfaces.Equipments; 6 | using TechTrack.Domain.Enums; 7 | using TechTrack.Domain.Models; 8 | 9 | namespace TechTrack.UnitTests.Application.Equipments.Commands.RetireEquipment 10 | { 11 | public class RetireEquipmentCommandHandlerTests 12 | { 13 | private readonly Mock _mockEquipmentRepository; 14 | private readonly RetireEquipmentCommandHandler _handler; 15 | 16 | public RetireEquipmentCommandHandlerTests() 17 | { 18 | _mockEquipmentRepository = new Mock(); 19 | _handler = new RetireEquipmentCommandHandler(_mockEquipmentRepository.Object); 20 | } 21 | 22 | [Fact] 23 | public async Task Handle_EquipmentExists_EquipmentRetiredSuccessfully() 24 | { 25 | // Arrange 26 | var equipmentId = Guid.NewGuid(); 27 | var command = new RetireEquipmentCommand(equipmentId); 28 | var equipment = new Equipment { Id = equipmentId }; // Assuming Equipment has a public constructor 29 | 30 | _mockEquipmentRepository.Setup(repo => repo.GetEquipmentAsync(equipmentId, It.IsAny())) 31 | .ReturnsAsync(equipment); 32 | 33 | _mockEquipmentRepository.Setup(repo => repo.Retire(equipmentId)) 34 | .Verifiable("Equipment was not retired."); 35 | 36 | _mockEquipmentRepository.Setup(repo => repo.SaveChangesAsync(It.IsAny())) 37 | .Returns(Task.CompletedTask) 38 | .Verifiable("Changes were not saved."); 39 | 40 | // Act 41 | await _handler.Handle(command, CancellationToken.None); 42 | 43 | // Assert 44 | _mockEquipmentRepository.Verify(); 45 | equipment.DomainEvents.Should().ContainSingle(e => e is EquipmentRetired); 46 | } 47 | 48 | [Fact] 49 | public async Task Handle_EquipmentDoesNotExist_ThrowsKeyNotFoundException() 50 | { 51 | // Arrange 52 | var nonExistentEquipmentId = Guid.NewGuid(); 53 | var command = new RetireEquipmentCommand(nonExistentEquipmentId); 54 | 55 | _mockEquipmentRepository.Setup(repo => repo.GetEquipmentAsync(nonExistentEquipmentId, It.IsAny())) 56 | .ReturnsAsync((Equipment)null); 57 | 58 | // Act & Assert 59 | await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/RetireEquipment/RetireEquipmentCommandValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.TestHelper; 2 | using TechTrack.Application.Equipments.Commands.DeleteEquipment; 3 | using Xunit; 4 | 5 | namespace TechTrack.Application.Tests.Equipments.Commands.RetireEquipment 6 | { 7 | public class RetireEquipmentCommandValidatorTests 8 | { 9 | private readonly RetireEquipmentCommandValidator _validator; 10 | 11 | public RetireEquipmentCommandValidatorTests() 12 | { 13 | _validator = new RetireEquipmentCommandValidator(); 14 | } 15 | 16 | [Fact] 17 | public void RetireEquipmentCommandValidator_WithEmptyId_ShouldHaveValidationError() 18 | { 19 | // Arrange 20 | var command = new RetireEquipmentCommand (Guid.Empty); 21 | 22 | // Act 23 | var result = _validator.TestValidate(command); 24 | 25 | // Assert 26 | result.ShouldHaveValidationErrorFor(command => command.Id); 27 | } 28 | 29 | [Fact] 30 | public void RetireEquipmentCommandValidator_WithValidId_ShouldNotHaveValidationError() 31 | { 32 | // Arrange 33 | var command = new RetireEquipmentCommand (Guid.NewGuid()); 34 | 35 | // Act 36 | var result = _validator.TestValidate(command); 37 | 38 | // Assert 39 | result.ShouldNotHaveValidationErrorFor(command => command.Id); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/UpdateEquipment/UpdateEquipmentCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Moq; 3 | using TechTrack.Application.Equipments.Commands.UpdateEquipment; 4 | using TechTrack.Common.Dtos.Equipments; 5 | using TechTrack.Application.Interfaces.Equipments; 6 | using TechTrack.Domain.Models; 7 | 8 | namespace TechTrack.UnitTests.Application.Equipments.Commands.UpdateEquipment 9 | { 10 | public class UpdateEquipmentCommandHandlerTests 11 | { 12 | private readonly Mock _mockEquipmentRepository; 13 | private readonly Mock _mockMapper; 14 | private readonly UpdateEquipmentCommandHandler _handler; 15 | 16 | public UpdateEquipmentCommandHandlerTests() 17 | { 18 | _mockEquipmentRepository = new Mock(); 19 | _mockMapper = new Mock(); 20 | _handler = new UpdateEquipmentCommandHandler(_mockEquipmentRepository.Object, _mockMapper.Object); 21 | } 22 | 23 | [Fact] 24 | public async Task Handle_ValidCommand_UpdatesEquipment() 25 | { 26 | // Arrange 27 | var equipmentId = Guid.NewGuid(); 28 | var equipmentDto = new EquipmentForUpdateDto(); 29 | var equipment = new Equipment(); 30 | 31 | _mockEquipmentRepository.Setup(repo => repo.GetEquipmentAsync(equipmentId, It.IsAny())) 32 | .ReturnsAsync(equipment); 33 | 34 | var command = new UpdateEquipmentCommand(equipmentId, equipmentDto); 35 | 36 | // Act 37 | await _handler.Handle(command, CancellationToken.None); 38 | 39 | // Assert 40 | _mockMapper.Verify(mapper => mapper.Map(equipmentDto, equipment), Times.Once); 41 | _mockEquipmentRepository.Verify(repo => repo.Update(equipment), Times.Once); 42 | _mockEquipmentRepository.Verify(repo => repo.SaveChangesAsync(It.IsAny()), Times.Once); 43 | } 44 | 45 | [Fact] 46 | public async Task Handle_InvalidEquipmentId_ThrowsKeyNotFoundException() 47 | { 48 | // Arrange 49 | var equipmentId = Guid.NewGuid(); 50 | var equipmentDto = new EquipmentForUpdateDto(); 51 | _mockEquipmentRepository.Setup(repo => repo.GetEquipmentAsync(equipmentId, It.IsAny())) 52 | .ReturnsAsync((Equipment)null); 53 | 54 | var command = new UpdateEquipmentCommand(equipmentId, equipmentDto); 55 | 56 | // Act & Assert 57 | await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Commands/UpdateEquipment/UpdateEquipmentCommandValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.TestHelper; 2 | using Moq; 3 | using TechTrack.Application.Equipments.Commands.UpdateEquipment; 4 | using TechTrack.Common.Dtos.Equipments; 5 | using TechTrack.Application.Interfaces.Equipments; 6 | using TechTrack.Domain.Models; 7 | 8 | namespace TechTrack.Application.Tests.Equipments.Commands.UpdateEquipment 9 | { 10 | public class UpdateEquipmentCommandValidatorTests 11 | { 12 | private readonly UpdateEquipmentCommandValidator _validator; 13 | private readonly Mock _mockEquipmentReadRepository; 14 | 15 | public UpdateEquipmentCommandValidatorTests() 16 | { 17 | _mockEquipmentReadRepository = new Mock(); 18 | _validator = new UpdateEquipmentCommandValidator(_mockEquipmentReadRepository.Object); 19 | } 20 | 21 | [Fact] 22 | public async Task Equipment_ShouldExist() 23 | { 24 | // Arrange 25 | var equipmentId = Guid.NewGuid(); 26 | _mockEquipmentReadRepository.Setup(x => x.GetByIdAsync(equipmentId, It.IsAny())) 27 | .ReturnsAsync(new Equipment()); 28 | 29 | var command = new UpdateEquipmentCommand(equipmentId, new EquipmentForUpdateDto()); 30 | 31 | // Act 32 | var result = await _validator.TestValidateAsync(command); 33 | 34 | // Assert 35 | result.ShouldNotHaveValidationErrorFor(c => c.Id); 36 | } 37 | 38 | [Fact] 39 | public async Task Equipment_ShouldNotExist() 40 | { 41 | // Arrange 42 | var nonExistentEquipmentId = Guid.NewGuid(); 43 | _mockEquipmentReadRepository.Setup(x => x.GetByIdAsync(nonExistentEquipmentId, It.IsAny())) 44 | .ReturnsAsync((Equipment)null); // Simulate that equipment does not exist 45 | 46 | var command = new UpdateEquipmentCommand(nonExistentEquipmentId, new EquipmentForUpdateDto()); 47 | 48 | // Act 49 | var result = await _validator.TestValidateAsync(command); 50 | 51 | // Assert 52 | result.ShouldHaveValidationErrorFor(c => c.Id).WithErrorMessage($"Equipment with ID {nonExistentEquipmentId} does not exist."); 53 | } 54 | 55 | 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Queries/GetEquipment/GetEquipmentQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoMapper; 3 | using FluentAssertions; 4 | using Moq; 5 | using TechTrack.Application; 6 | using TechTrack.Common.Dtos.Equipments; 7 | using TechTrack.Application.Equipments.Queries.GetEquipment; 8 | using TechTrack.Application.Interfaces.Equipments; 9 | using TechTrack.Domain.Models; 10 | 11 | namespace TechTrack.UnitTests.Application.Equipments.Queries.GetEquipment 12 | { 13 | public class GetEquipmentQueryHandlerTests 14 | { 15 | private readonly Mock _mockEquipmentRepository; 16 | private readonly IMapper _mapper; 17 | private readonly GetEquipmentQueryHandler _handler; 18 | 19 | public GetEquipmentQueryHandlerTests() 20 | { 21 | _mockEquipmentRepository = new Mock(); 22 | // Setup AutoMapper 23 | var configuration = new MapperConfiguration(cfg => 24 | { 25 | cfg.AddProfile(); 26 | }); 27 | _mapper = configuration.CreateMapper(); 28 | _handler = new GetEquipmentQueryHandler(_mockEquipmentRepository.Object, _mapper); 29 | } 30 | 31 | [Fact] 32 | public async Task Handle_ValidId_ReturnsMappedEquipmentDto() 33 | { 34 | // Arrange 35 | var equipmentId = Guid.NewGuid(); 36 | var equipment = new Equipment { Id = equipmentId, Name = "Test Equipment" }; 37 | 38 | _mockEquipmentRepository.Setup(repo => repo.GetByIdAsync(equipmentId, It.IsAny())) 39 | .ReturnsAsync(equipment); 40 | 41 | var query = new GetEquipmentQuery(equipmentId); 42 | 43 | // Act 44 | var result = await _handler.Handle(query, CancellationToken.None); 45 | 46 | // Assert 47 | result.Should().NotBeNull(); 48 | result.Id.Should().Be(equipmentId); 49 | result.Name.Should().Be("Test Equipment"); 50 | _mockEquipmentRepository.Verify(repo => repo.GetByIdAsync(equipmentId, It.IsAny()), Times.Once); 51 | } 52 | 53 | [Fact] 54 | public async Task Handle_InvalidId_ReturnsNull() 55 | { 56 | // Arrange 57 | var invalidEquipmentId = Guid.NewGuid(); 58 | 59 | _mockEquipmentRepository.Setup(repo => repo.GetByIdAsync(invalidEquipmentId, It.IsAny())) 60 | .ReturnsAsync((Equipment)null); 61 | 62 | var query = new GetEquipmentQuery(invalidEquipmentId); 63 | 64 | // Act 65 | var result = await _handler.Handle(query, CancellationToken.None); 66 | 67 | // Assert 68 | result.Should().BeNull(); 69 | } 70 | 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /TechTrack.UnitTests/Application/Equipments/Queries/GetEquipments/GetEquipmentsQueryHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoMapper; 3 | using FluentAssertions; 4 | using Moq; 5 | using TechTrack.Application; 6 | using TechTrack.Common.Pagination; 7 | using TechTrack.Common.Dtos.Equipments; 8 | using TechTrack.Application.Equipments.Queries.GetEquipments; 9 | using TechTrack.Common.ViewModel.Equipments; 10 | using TechTrack.Application.Interfaces.Equipments; 11 | 12 | namespace TechTrack.UnitTests.Application.Equipments.Queries.GetEquipments 13 | { 14 | public class GetEquipmentsQueryHandlerTests 15 | { 16 | private readonly Mock _mockEquipmentReadRepository; 17 | private readonly IMapper _mapper; 18 | private readonly GetEquipmentsQueryHandler _handler; 19 | private readonly Fixture _fixture = new(); 20 | 21 | public GetEquipmentsQueryHandlerTests() 22 | { 23 | _mockEquipmentReadRepository = new Mock(); 24 | // Setup AutoMapper 25 | var configuration = new MapperConfiguration(cfg => 26 | { 27 | cfg.AddProfile(); 28 | }); 29 | _mapper = configuration.CreateMapper(); 30 | _handler = new GetEquipmentsQueryHandler(_mockEquipmentReadRepository.Object, _mapper); 31 | } 32 | 33 | [Fact] 34 | public async Task Handle_ValidFilter_ReturnsPaginatedResult() 35 | { 36 | // Arrange 37 | const int pageNumber = 1; 38 | const int pageSize = 10; 39 | 40 | var equipmentInputVm = new EquipmentInputVm 41 | { 42 | PageNumber = pageNumber, 43 | PageSize = pageSize 44 | }; 45 | var data = _fixture.CreateMany() 46 | .ToList(); 47 | 48 | var request = new GetEquipmentsQuery(equipmentInputVm); 49 | 50 | var paginatedResult = new PaginatedResult(data, 1, 10, 100); 51 | _mockEquipmentReadRepository.Setup(repo => repo.FilterEquipmentsAsync(It.IsAny(), It.IsAny())) 52 | .ReturnsAsync(paginatedResult); 53 | 54 | // Act 55 | var result = await _handler.Handle(request, CancellationToken.None); 56 | 57 | // Assert 58 | result.Should().NotBeNull(); 59 | result.Data.Should().NotBeEmpty(); 60 | result.PageNumber.Should().Be(1); 61 | result.PageSize.Should().Be(10); 62 | result.TotalRecords.Should().Be(100); 63 | } 64 | 65 | [Fact] 66 | public async Task Handle_NoEquipmentsFound_ReturnsEmptyPaginatedResult() 67 | { 68 | // Arrange 69 | var request = new GetEquipmentsQuery(new EquipmentInputVm()); 70 | 71 | var paginatedResult = new PaginatedResult(new List(), 1, 10, 0); 72 | _mockEquipmentReadRepository.Setup(repo => repo.FilterEquipmentsAsync(It.IsAny(), It.IsAny())) 73 | .ReturnsAsync(paginatedResult); 74 | 75 | // Act 76 | var result = await _handler.Handle(request, CancellationToken.None); 77 | 78 | // Assert 79 | result.Should().NotBeNull(); 80 | result.Data.Should().BeEmpty(); 81 | result.TotalRecords.Should().Be(0); 82 | } 83 | 84 | } 85 | } -------------------------------------------------------------------------------- /TechTrack.UnitTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /TechTrack.UnitTests/TechTrack.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Components/Pages/Auth.razor: -------------------------------------------------------------------------------- 1 | @page "/auth" 2 | 3 | @using Microsoft.AspNetCore.Authorization 4 | 5 | @attribute [Authorize] 6 | @rendermode InteractiveAuto 7 | 8 | Auth 9 | 10 |

You are authenticated

11 | 12 | 13 | Hello @context.User.Identity?.Name! 14 | 15 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Components/Pages/Equipments/AddEquipment.razor: -------------------------------------------------------------------------------- 1 | @page "/equipment/add" 2 | @using TechTrack.Common.Dtos.Equipments 3 | @using TechTrack.Common.Interfaces.HttpClients 4 | @inject IEquipmentsHttpClientService EquipmentsService 5 | @inject NavigationManager NavigationManager 6 | 7 | @rendermode InteractiveAuto 8 | 9 |

Add Equipment

10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 |
35 | 36 | @code { 37 | private EquipmentForCreationDto equipmentDto = new(); 38 | 39 | private async Task HandleValidSubmit() 40 | { 41 | await EquipmentsService.CreateEquipmentAsync(equipmentDto); 42 | NavigationManager.NavigateTo("/equipment"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Components/Pages/Equipments/Equipment.razor: -------------------------------------------------------------------------------- 1 | @page "/equipment" 2 | @using TechTrack.Common.Dtos.Equipments 3 | @using TechTrack.Common.Interfaces.HttpClients 4 | @using TechTrack.Common.Pagination 5 | @using TechTrack.Common.ViewModel.Equipments 6 | @using TechTrack.Domain.Enums 7 | 8 | @inject IEquipmentsHttpClientService EquipmentsService 9 | @inject NavigationManager NavigationManager 10 | 11 | @rendermode InteractiveAuto 12 | 13 |

Equipments

14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 33 |
34 |
35 | 36 | @if (equipments == null) 37 | { 38 |

Loading...

39 | } 40 | else if (equipments?.Data?.Count == 0) 41 | { 42 |

No equipments found. You can add new equipments.

43 | } 44 | else 45 | { 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | @foreach (var equipment in equipments?.Data) 57 | { 58 | 59 | 60 | 61 | 62 | 66 | 67 | } 68 | 69 |
NameTypeStatusActions
@equipment.Name@equipment.Type@equipment.Status 63 | 64 | 65 |
70 | 71 | 81 | 82 | } 83 | 84 | 85 | 86 | 87 | @code { 88 | private PaginatedResult? equipments; 89 | private EquipmentInputVm filter = new(); 90 | private int currentPage = 1; 91 | 92 | protected override async Task OnInitializedAsync() 93 | { 94 | await LoadEquipments(); 95 | } 96 | 97 | private async Task LoadEquipments() 98 | { 99 | equipments = await EquipmentsService.GetEquipmentsAsync(filter); 100 | } 101 | 102 | private async Task AddEquipment() 103 | { 104 | NavigationManager.NavigateTo("/equipment/add"); 105 | } 106 | 107 | private async Task UpdateEquipment(Guid id) 108 | { 109 | NavigationManager.NavigateTo($"/equipment/update/{id}"); 110 | } 111 | 112 | private async Task RetireEquipment(Guid id) 113 | { 114 | await EquipmentsService.RetireEquipmentAsync(id); 115 | await LoadEquipments(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Components/Pages/Users/UsersWithEquipments.razor: -------------------------------------------------------------------------------- 1 | @page "/users-with-equipment" 2 | @inject HttpClient Http 3 | @using TechTrack.Common.ViewModel.Equipments 4 | @using TechTrack.Common.ViewModel.Users 5 | 6 |

Users and Their Equipment

7 | 8 | @if (usersWithEquipment == null) 9 | { 10 |

Loading...

11 | } 12 | else 13 | { 14 | @foreach (var user in usersWithEquipment) 15 | { 16 |
17 |

@user.Name (@user.Department)

18 | @if (user.Equipments.Any()) 19 | { 20 |
    21 | @foreach (var eq in user.Equipments) 22 | { 23 |
  • @eq.Name - @eq.Status
  • 24 | } 25 |
26 | } 27 | else 28 | { 29 |

This user has no equipment assigned.

30 | } 31 |
32 | } 33 | } 34 | 35 | @code { 36 | private List? usersWithEquipment; 37 | 38 | protected override async Task OnInitializedAsync() 39 | { 40 | usersWithEquipment = await Http.GetFromJsonAsync>("api/users"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/PersistentAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using System.Security.Claims; 4 | using TechTrack.Common.Dtos.Users; 5 | using TechTrack.Common.Interfaces.HttpClients; 6 | 7 | namespace Techtrack.Ui.Client; 8 | // This is a client-side AuthenticationStateProvider that determines the user's authentication state by 9 | // looking for data persisted in the page when it was rendered on the server. This authentication state will 10 | // be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full 11 | // page reload is required. 12 | // 13 | // This only provides a user name and email for display purposes. It does not actually include any tokens 14 | // that authenticate to the server when making subsequent requests. That works separately using a 15 | // cookie that will be included on HttpClient requests to the server. 16 | internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider 17 | { 18 | private static readonly Task defaultUnauthenticatedTask = 19 | Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); 20 | 21 | private readonly Task authenticationStateTask = defaultUnauthenticatedTask; 22 | 23 | private readonly IUserHttpClientService _clientService; 24 | 25 | public PersistentAuthenticationStateProvider(PersistentComponentState state, IUserHttpClientService clientService) 26 | { 27 | _clientService = clientService; 28 | 29 | if (!state.TryTakeFromJson(nameof(UserInfo), out var userInfo) || userInfo is null) 30 | { 31 | return; 32 | } 33 | 34 | Claim[] claims = [ 35 | new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), 36 | new Claim(ClaimTypes.Name, userInfo.Name), 37 | new Claim(ClaimTypes.Email, userInfo.Email)]; 38 | 39 | authenticationStateTask = Task.FromResult( 40 | new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, 41 | authenticationType: nameof(PersistentAuthenticationStateProvider))))); 42 | 43 | _clientService.AddUserAsync(new UserForCreationDto 44 | { 45 | Name = userInfo.Name, 46 | Email = userInfo.Email, 47 | }); 48 | } 49 | 50 | public override Task GetAuthenticationStateAsync() => authenticationStateTask; 51 | } 52 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Authorization; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 3 | using Techtrack.Ui.Client; 4 | using Techtrack.Ui.Client.Services; 5 | using TechTrack.Common.Interfaces.HttpClients; 6 | 7 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 8 | 9 | builder.Services.AddAuthorizationCore(); 10 | builder.Services.AddCascadingAuthenticationState(); 11 | builder.Services.AddSingleton(); 12 | 13 | builder.Services.AddScoped(sp => 14 | { 15 | var httpClient = new HttpClient { BaseAddress = new Uri("https://localhost:7212/") }; 16 | return new UsersHttpClientService(httpClient); 17 | }); 18 | 19 | builder.Services.AddScoped(sp => 20 | { 21 | var httpClient = new HttpClient { BaseAddress = new Uri("https://localhost:7212/") }; 22 | return new EquipmentsHttpClientService(httpClient); 23 | }); 24 | 25 | await builder.Build().RunAsync(); 26 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Services/UsersHttpClientService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.WebUtilities; 2 | using System.Net.Http.Json; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using TechTrack.Common.Dtos.Equipments; 6 | using TechTrack.Common.Dtos.Users; 7 | using TechTrack.Common.Interfaces.HttpClients; 8 | using TechTrack.Common.Pagination; 9 | using TechTrack.Common.ViewModel.Equipments; 10 | using TechTrack.Common.ViewModel.Users; 11 | 12 | namespace Techtrack.Ui.Client.Services 13 | { 14 | public class UsersHttpClientService : IUserHttpClientService 15 | { 16 | private readonly HttpClient _httpClient; 17 | 18 | public UsersHttpClientService(HttpClient httpClient) 19 | { 20 | _httpClient = httpClient; 21 | } 22 | 23 | public async Task AddUserAsync(UserForCreationDto userForCreationDto) 24 | { 25 | var response = await _httpClient.PostAsJsonAsync("api/v1/users", userForCreationDto); 26 | response.EnsureSuccessStatusCode(); 27 | } 28 | 29 | public async Task> GetUsersWithEquipmentsAsync() 30 | { 31 | var options = new JsonSerializerOptions 32 | { 33 | PropertyNameCaseInsensitive = true, 34 | }; 35 | 36 | options.Converters.Add(new JsonStringEnumConverter()); 37 | 38 | var response = await _httpClient.GetAsync("api/v1/users/with-equipments"); 39 | var content = await response.Content.ReadAsStringAsync(); 40 | 41 | if (!response.IsSuccessStatusCode) 42 | { 43 | throw new ApplicationException(content); 44 | } 45 | 46 | var users = JsonSerializer.Deserialize>(content, options); 47 | 48 | return users ?? []; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/Techtrack.Ui.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Default 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/UserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Techtrack.Ui.Client; 2 | 3 | // Add properties to this class and update the server and client AuthenticationStateProviders 4 | // to expose more information about the authenticated user to the client. 5 | public class UserInfo 6 | { 7 | public required string UserId { get; set; } 8 | public required string Email { get; set; } 9 | public required string Name { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.JSInterop 10 | @using Techtrack.Ui.Client 11 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "DetailedErrors": true 9 | } 10 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui.Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "DetailedErrors": true 9 | } 10 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/IdentityNoOpEmailSender.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.UI.Services; 3 | using Techtrack.Ui.Data; 4 | 5 | namespace Techtrack.Ui.Components.Account; 6 | // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. 7 | internal sealed class IdentityNoOpEmailSender : IEmailSender 8 | { 9 | private readonly IEmailSender emailSender = new NoOpEmailSender(); 10 | 11 | public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => 12 | emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); 13 | 14 | public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => 15 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); 16 | 17 | public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => 18 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); 19 | } 20 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/IdentityRedirectManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Techtrack.Ui.Components.Account; 5 | internal sealed class IdentityRedirectManager(NavigationManager navigationManager) 6 | { 7 | public const string StatusCookieName = "Identity.StatusMessage"; 8 | 9 | private static readonly CookieBuilder StatusCookieBuilder = new() 10 | { 11 | SameSite = SameSiteMode.Strict, 12 | HttpOnly = true, 13 | IsEssential = true, 14 | MaxAge = TimeSpan.FromSeconds(5), 15 | }; 16 | 17 | [DoesNotReturn] 18 | public void RedirectTo(string? uri) 19 | { 20 | uri ??= ""; 21 | 22 | // Prevent open redirects. 23 | if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) 24 | { 25 | uri = navigationManager.ToBaseRelativePath(uri); 26 | } 27 | 28 | // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. 29 | // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. 30 | navigationManager.NavigateTo(uri); 31 | throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); 32 | } 33 | 34 | [DoesNotReturn] 35 | public void RedirectTo(string uri, Dictionary queryParameters) 36 | { 37 | var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); 38 | var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); 39 | RedirectTo(newUri); 40 | } 41 | 42 | [DoesNotReturn] 43 | public void RedirectToWithStatus(string uri, string message, HttpContext context) 44 | { 45 | context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); 46 | RedirectTo(uri); 47 | } 48 | 49 | private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); 50 | 51 | [DoesNotReturn] 52 | public void RedirectToCurrentPage() => RedirectTo(CurrentPath); 53 | 54 | [DoesNotReturn] 55 | public void RedirectToCurrentPageWithStatus(string message, HttpContext context) 56 | => RedirectToWithStatus(CurrentPath, message, context); 57 | } 58 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/IdentityUserAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Techtrack.Ui.Data; 3 | 4 | namespace Techtrack.Ui.Components.Account; 5 | internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager) 6 | { 7 | public async Task GetRequiredUserAsync(HttpContext context) 8 | { 9 | var user = await userManager.GetUserAsync(context.User); 10 | 11 | if (user is null) 12 | { 13 | redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); 14 | } 15 | 16 | return user; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ConfirmEmail.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmail" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using Techtrack.Ui.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | 11 | Confirm email 12 | 13 |

Confirm email

14 | 15 | 16 | @code { 17 | private string? statusMessage; 18 | 19 | [CascadingParameter] 20 | private HttpContext HttpContext { get; set; } = default!; 21 | 22 | [SupplyParameterFromQuery] 23 | private string? UserId { get; set; } 24 | 25 | [SupplyParameterFromQuery] 26 | private string? Code { get; set; } 27 | 28 | protected override async Task OnInitializedAsync() 29 | { 30 | if (UserId is null || Code is null) 31 | { 32 | RedirectManager.RedirectTo(""); 33 | } 34 | 35 | var user = await UserManager.FindByIdAsync(UserId); 36 | if (user is null) 37 | { 38 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 39 | statusMessage = $"Error loading user with ID {UserId}"; 40 | } 41 | else 42 | { 43 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 44 | var result = await UserManager.ConfirmEmailAsync(user, code); 45 | statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ConfirmEmailChange.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmailChange" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using Techtrack.Ui.Data 7 | 8 | @inject UserManager UserManager 9 | @inject SignInManager SignInManager 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Confirm email change 13 | 14 |

Confirm email change

15 | 16 | 17 | 18 | @code { 19 | private string? message; 20 | 21 | [CascadingParameter] 22 | private HttpContext HttpContext { get; set; } = default!; 23 | 24 | [SupplyParameterFromQuery] 25 | private string? UserId { get; set; } 26 | 27 | [SupplyParameterFromQuery] 28 | private string? Email { get; set; } 29 | 30 | [SupplyParameterFromQuery] 31 | private string? Code { get; set; } 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | if (UserId is null || Email is null || Code is null) 36 | { 37 | RedirectManager.RedirectToWithStatus( 38 | "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); 39 | } 40 | 41 | var user = await UserManager.FindByIdAsync(UserId); 42 | if (user is null) 43 | { 44 | message = "Unable to find user with Id '{userId}'"; 45 | return; 46 | } 47 | 48 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 49 | var result = await UserManager.ChangeEmailAsync(user, Email, code); 50 | if (!result.Succeeded) 51 | { 52 | message = "Error changing email."; 53 | return; 54 | } 55 | 56 | // In our UI email and user name are one and the same, so when we update the email 57 | // we need to update the user name. 58 | var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); 59 | if (!setUserNameResult.Succeeded) 60 | { 61 | message = "Error changing user name."; 62 | return; 63 | } 64 | 65 | await SignInManager.RefreshSignInAsync(user); 66 | message = "Thank you for confirming your email change."; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ForgotPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using Techtrack.Ui.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Forgot your password? 16 | 17 |

Forgot your password?

18 |

Enter your email.

19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | [SupplyParameterFromForm] 38 | private InputModel Input { get; set; } = new(); 39 | 40 | private async Task OnValidSubmitAsync() 41 | { 42 | var user = await UserManager.FindByEmailAsync(Input.Email); 43 | if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) 44 | { 45 | // Don't reveal that the user does not exist or is not confirmed 46 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 47 | } 48 | 49 | // For more information on how to enable account confirmation and password reset please 50 | // visit https://go.microsoft.com/fwlink/?LinkID=532713 51 | var code = await UserManager.GeneratePasswordResetTokenAsync(user); 52 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 53 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 54 | NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, 55 | new Dictionary { ["code"] = code }); 56 | 57 | await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 58 | 59 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 60 | } 61 | 62 | private sealed class InputModel 63 | { 64 | [Required] 65 | [EmailAddress] 66 | public string Email { get; set; } = ""; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ForgotPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPasswordConfirmation" 2 | 3 | Forgot password confirmation 4 | 5 |

Forgot password confirmation

6 |

7 | Please check your email to reset your password. 8 |

9 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/InvalidPasswordReset.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidPasswordReset" 2 | 3 | Invalid password reset 4 | 5 |

Invalid password reset

6 |

7 | The password reset link is invalid. 8 |

9 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/InvalidUser.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidUser" 2 | 3 | Invalid user 4 | 5 |

Invalid user

6 | 7 | 8 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Lockout.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Lockout" 2 | 3 | Locked out 4 | 5 |
6 |

Locked out

7 |

This account has been locked out, please try again later.

8 |
9 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/LoginWithRecoveryCode.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/LoginWithRecoveryCode" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using Techtrack.Ui.Data 6 | 7 | @inject SignInManager SignInManager 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Recovery code verification 13 | 14 |

Recovery code verification

15 |
16 | 17 |

18 | You have requested to log in with a recovery code. This login will not be remembered until you provide 19 | an authenticator app code at log in or disable 2FA and log in again. 20 |

21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | private ApplicationUser user = default!; 39 | 40 | [SupplyParameterFromForm] 41 | private InputModel Input { get; set; } = new(); 42 | 43 | [SupplyParameterFromQuery] 44 | private string? ReturnUrl { get; set; } 45 | 46 | protected override async Task OnInitializedAsync() 47 | { 48 | // Ensure the user has gone through the username & password screen first 49 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? 50 | throw new InvalidOperationException("Unable to load two-factor authentication user."); 51 | } 52 | 53 | private async Task OnValidSubmitAsync() 54 | { 55 | var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); 56 | 57 | var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); 58 | 59 | var userId = await UserManager.GetUserIdAsync(user); 60 | 61 | if (result.Succeeded) 62 | { 63 | Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); 64 | RedirectManager.RedirectTo(ReturnUrl); 65 | } 66 | else if (result.IsLockedOut) 67 | { 68 | Logger.LogWarning("User account locked out."); 69 | RedirectManager.RedirectTo("Account/Lockout"); 70 | } 71 | else 72 | { 73 | Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); 74 | message = "Error: Invalid recovery code entered."; 75 | } 76 | } 77 | 78 | private sealed class InputModel 79 | { 80 | [Required] 81 | [DataType(DataType.Text)] 82 | [Display(Name = "Recovery Code")] 83 | public string RecoveryCode { get; set; } = ""; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/DeletePersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/DeletePersonalData" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using Techtrack.Ui.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | @inject ILogger Logger 12 | 13 | Delete Personal Data 14 | 15 | 16 | 17 |

Delete Personal Data

18 | 19 | 24 | 25 |
26 | 27 | 28 | 29 | @if (requirePassword) 30 | { 31 |
32 | 33 | 34 | 35 |
36 | } 37 | 38 |
39 |
40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private bool requirePassword; 45 | 46 | [CascadingParameter] 47 | private HttpContext HttpContext { get; set; } = default!; 48 | 49 | [SupplyParameterFromForm] 50 | private InputModel Input { get; set; } = new(); 51 | 52 | protected override async Task OnInitializedAsync() 53 | { 54 | Input ??= new(); 55 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 56 | requirePassword = await UserManager.HasPasswordAsync(user); 57 | } 58 | 59 | private async Task OnValidSubmitAsync() 60 | { 61 | if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) 62 | { 63 | message = "Error: Incorrect password."; 64 | return; 65 | } 66 | 67 | var result = await UserManager.DeleteAsync(user); 68 | if (!result.Succeeded) 69 | { 70 | throw new InvalidOperationException("Unexpected error occurred deleting user."); 71 | } 72 | 73 | await SignInManager.SignOutAsync(); 74 | 75 | var userId = await UserManager.GetUserIdAsync(user); 76 | Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); 77 | 78 | RedirectManager.RedirectToCurrentPage(); 79 | } 80 | 81 | private sealed class InputModel 82 | { 83 | [DataType(DataType.Password)] 84 | public string Password { get; set; } = ""; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/Disable2fa.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/Disable2fa" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using Techtrack.Ui.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Disable two-factor authentication (2FA) 12 | 13 | 14 |

Disable two-factor authentication (2FA)

15 | 16 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | private ApplicationUser user = default!; 35 | 36 | [CascadingParameter] 37 | private HttpContext HttpContext { get; set; } = default!; 38 | 39 | protected override async Task OnInitializedAsync() 40 | { 41 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 42 | 43 | if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) 44 | { 45 | throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); 46 | } 47 | } 48 | 49 | private async Task OnSubmitAsync() 50 | { 51 | var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); 52 | if (!disable2faResult.Succeeded) 53 | { 54 | throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); 55 | } 56 | 57 | var userId = await UserManager.GetUserIdAsync(user); 58 | Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); 59 | RedirectManager.RedirectToWithStatus( 60 | "Account/Manage/TwoFactorAuthentication", 61 | "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", 62 | HttpContext); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/GenerateRecoveryCodes" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using Techtrack.Ui.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Generate two-factor authentication (2FA) recovery codes 12 | 13 | @if (recoveryCodes is not null) 14 | { 15 | 16 | } 17 | else 18 | { 19 |

Generate two-factor authentication (2FA) recovery codes

20 | 33 |
34 |
35 | 36 | 37 | 38 |
39 | } 40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private IEnumerable? recoveryCodes; 45 | 46 | [CascadingParameter] 47 | private HttpContext HttpContext { get; set; } = default!; 48 | 49 | protected override async Task OnInitializedAsync() 50 | { 51 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 52 | 53 | var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 54 | if (!isTwoFactorEnabled) 55 | { 56 | throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled."); 57 | } 58 | } 59 | 60 | private async Task OnSubmitAsync() 61 | { 62 | var userId = await UserManager.GetUserIdAsync(user); 63 | recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 64 | message = "You have generated new recovery codes."; 65 | 66 | Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using Techtrack.Ui.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Profile 13 | 14 |

Profile

15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private ApplicationUser user = default!; 38 | private string? username; 39 | private string? phoneNumber; 40 | 41 | [CascadingParameter] 42 | private HttpContext HttpContext { get; set; } = default!; 43 | 44 | [SupplyParameterFromForm] 45 | private InputModel Input { get; set; } = new(); 46 | 47 | protected override async Task OnInitializedAsync() 48 | { 49 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 50 | username = await UserManager.GetUserNameAsync(user); 51 | phoneNumber = await UserManager.GetPhoneNumberAsync(user); 52 | 53 | Input.PhoneNumber ??= phoneNumber; 54 | } 55 | 56 | private async Task OnValidSubmitAsync() 57 | { 58 | if (Input.PhoneNumber != phoneNumber) 59 | { 60 | var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); 61 | if (!setPhoneResult.Succeeded) 62 | { 63 | RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); 64 | } 65 | } 66 | 67 | await SignInManager.RefreshSignInAsync(user); 68 | RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); 69 | } 70 | 71 | private sealed class InputModel 72 | { 73 | [Phone] 74 | [Display(Name = "Phone number")] 75 | public string? PhoneNumber { get; set; } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/PersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/PersonalData" 2 | 3 | @inject IdentityUserAccessor UserAccessor 4 | 5 | Personal Data 6 | 7 | 8 |

Personal Data

9 | 10 |
11 |
12 |

Your account contains personal data that you have given us. This page allows you to download or delete that data.

13 |

14 | Deleting this data will permanently remove your account, and this cannot be recovered. 15 |

16 |
17 | 18 | 19 | 20 |

21 | Delete 22 |

23 |
24 |
25 | 26 | @code { 27 | [CascadingParameter] 28 | private HttpContext HttpContext { get; set; } = default!; 29 | 30 | protected override async Task OnInitializedAsync() 31 | { 32 | _ = await UserAccessor.GetRequiredUserAsync(HttpContext); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/ResetAuthenticator.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ResetAuthenticator" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using Techtrack.Ui.Data 5 | 6 | @inject UserManager UserManager 7 | @inject SignInManager SignInManager 8 | @inject IdentityUserAccessor UserAccessor 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Reset authenticator key 13 | 14 | 15 |

Reset authenticator key

16 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | [CascadingParameter] 35 | private HttpContext HttpContext { get; set; } = default!; 36 | 37 | private async Task OnSubmitAsync() 38 | { 39 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 40 | await UserManager.SetTwoFactorEnabledAsync(user, false); 41 | await UserManager.ResetAuthenticatorKeyAsync(user); 42 | var userId = await UserManager.GetUserIdAsync(user); 43 | Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); 44 | 45 | await SignInManager.RefreshSignInAsync(user); 46 | 47 | RedirectManager.RedirectToWithStatus( 48 | "Account/Manage/EnableAuthenticator", 49 | "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", 50 | HttpContext); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/SetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/SetPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using Techtrack.Ui.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Set password 13 | 14 |

Set your password

15 | 16 |

17 | You do not have a local username/password for this site. Add a local 18 | account so you can log in without an external login. 19 |

20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | @code { 41 | private string? message; 42 | private ApplicationUser user = default!; 43 | 44 | [CascadingParameter] 45 | private HttpContext HttpContext { get; set; } = default!; 46 | 47 | [SupplyParameterFromForm] 48 | private InputModel Input { get; set; } = new(); 49 | 50 | protected override async Task OnInitializedAsync() 51 | { 52 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 53 | 54 | var hasPassword = await UserManager.HasPasswordAsync(user); 55 | if (hasPassword) 56 | { 57 | RedirectManager.RedirectTo("Account/Manage/ChangePassword"); 58 | } 59 | } 60 | 61 | private async Task OnValidSubmitAsync() 62 | { 63 | var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); 64 | if (!addPasswordResult.Succeeded) 65 | { 66 | message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; 67 | return; 68 | } 69 | 70 | await SignInManager.RefreshSignInAsync(user); 71 | RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); 72 | } 73 | 74 | private sealed class InputModel 75 | { 76 | [Required] 77 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 78 | [DataType(DataType.Password)] 79 | [Display(Name = "New password")] 80 | public string? NewPassword { get; set; } 81 | 82 | [DataType(DataType.Password)] 83 | [Display(Name = "Confirm new password")] 84 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 85 | public string? ConfirmPassword { get; set; } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/TwoFactorAuthentication.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/TwoFactorAuthentication" 2 | 3 | @using Microsoft.AspNetCore.Http.Features 4 | @using Microsoft.AspNetCore.Identity 5 | @using Techtrack.Ui.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Two-factor authentication (2FA) 13 | 14 | 15 |

Two-factor authentication (2FA)

16 | @if (canTrack) 17 | { 18 | if (is2faEnabled) 19 | { 20 | if (recoveryCodesLeft == 0) 21 | { 22 |
23 | You have no recovery codes left. 24 |

You must generate a new set of recovery codes before you can log in with a recovery code.

25 |
26 | } 27 | else if (recoveryCodesLeft == 1) 28 | { 29 |
30 | You have 1 recovery code left. 31 |

You can generate a new set of recovery codes.

32 |
33 | } 34 | else if (recoveryCodesLeft <= 3) 35 | { 36 |
37 | You have @recoveryCodesLeft recovery codes left. 38 |

You should generate a new set of recovery codes.

39 |
40 | } 41 | 42 | if (isMachineRemembered) 43 | { 44 |
45 | 46 | 47 | 48 | } 49 | 50 | Disable 2FA 51 | Reset recovery codes 52 | } 53 | 54 |

Authenticator app

55 | @if (!hasAuthenticator) 56 | { 57 | Add authenticator app 58 | } 59 | else 60 | { 61 | Set up authenticator app 62 | Reset authenticator app 63 | } 64 | } 65 | else 66 | { 67 |
68 | Privacy and cookie policy have not been accepted. 69 |

You must accept the policy before you can enable two factor authentication.

70 |
71 | } 72 | 73 | @code { 74 | private bool canTrack; 75 | private bool hasAuthenticator; 76 | private int recoveryCodesLeft; 77 | private bool is2faEnabled; 78 | private bool isMachineRemembered; 79 | 80 | [CascadingParameter] 81 | private HttpContext HttpContext { get; set; } = default!; 82 | 83 | protected override async Task OnInitializedAsync() 84 | { 85 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 86 | canTrack = HttpContext.Features.Get()?.CanTrack ?? true; 87 | hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; 88 | is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 89 | isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); 90 | recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); 91 | } 92 | 93 | private async Task OnSubmitForgetBrowserAsync() 94 | { 95 | await SignInManager.ForgetTwoFactorClientAsync(); 96 | 97 | RedirectManager.RedirectToCurrentPageWithStatus( 98 | "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", 99 | HttpContext); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/Manage/_Imports.razor: -------------------------------------------------------------------------------- 1 | @layout ManageLayout 2 | @attribute [Microsoft.AspNetCore.Authorization.Authorize] 3 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/RegisterConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/RegisterConfirmation" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using Techtrack.Ui.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IEmailSender EmailSender 10 | @inject NavigationManager NavigationManager 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Register confirmation 14 | 15 |

Register confirmation

16 | 17 | 18 | 19 | @if (emailConfirmationLink is not null) 20 | { 21 |

22 | This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. 23 | Normally this would be emailed: Click here to confirm your account 24 |

25 | } 26 | else 27 | { 28 |

Please check your email to confirm your account.

29 | } 30 | 31 | @code { 32 | private string? emailConfirmationLink; 33 | private string? statusMessage; 34 | 35 | [CascadingParameter] 36 | private HttpContext HttpContext { get; set; } = default!; 37 | 38 | [SupplyParameterFromQuery] 39 | private string? Email { get; set; } 40 | 41 | [SupplyParameterFromQuery] 42 | private string? ReturnUrl { get; set; } 43 | 44 | protected override async Task OnInitializedAsync() 45 | { 46 | if (Email is null) 47 | { 48 | RedirectManager.RedirectTo(""); 49 | } 50 | 51 | var user = await UserManager.FindByEmailAsync(Email); 52 | if (user is null) 53 | { 54 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 55 | statusMessage = "Error finding user for unspecified email"; 56 | } 57 | else if (EmailSender is IdentityNoOpEmailSender) 58 | { 59 | // Once you add a real email sender, you should remove this code that lets you confirm the account 60 | var userId = await UserManager.GetUserIdAsync(user); 61 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 62 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 63 | emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( 64 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 65 | new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ResendEmailConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResendEmailConfirmation" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using Techtrack.Ui.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Resend email confirmation 16 | 17 |

Resend email confirmation

18 |

Enter your email.

19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | 39 | [SupplyParameterFromForm] 40 | private InputModel Input { get; set; } = new(); 41 | 42 | private async Task OnValidSubmitAsync() 43 | { 44 | var user = await UserManager.FindByEmailAsync(Input.Email!); 45 | if (user is null) 46 | { 47 | message = "Verification email sent. Please check your email."; 48 | return; 49 | } 50 | 51 | var userId = await UserManager.GetUserIdAsync(user); 52 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 53 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 54 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 55 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 56 | new Dictionary { ["userId"] = userId, ["code"] = code }); 57 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 58 | 59 | message = "Verification email sent. Please check your email."; 60 | } 61 | 62 | private sealed class InputModel 63 | { 64 | [Required] 65 | [EmailAddress] 66 | public string Email { get; set; } = ""; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/ResetPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResetPasswordConfirmation" 2 | Reset password confirmation 3 | 4 |

Reset password confirmation

5 |

6 | Your password has been reset. Please click here to log in. 7 |

8 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Techtrack.Ui.Components.Account.Shared 2 | @layout AccountLayout 3 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/AccountLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout Techtrack.Ui.Components.Layout.MainLayout 3 | @inject NavigationManager NavigationManager 4 | 5 | @if (HttpContext is null) 6 | { 7 |

Loading...

8 | } 9 | else 10 | { 11 | @Body 12 | } 13 | 14 | @code { 15 | [CascadingParameter] 16 | private HttpContext? HttpContext { get; set; } 17 | 18 | protected override void OnParametersSet() 19 | { 20 | if (HttpContext is null) 21 | { 22 | // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext. 23 | // The identity pages need to set cookies, so they require an HttpContext. To achieve this we 24 | // must transition back from interactive mode to a server-rendered page. 25 | NavigationManager.Refresh(forceReload: true); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/ExternalLoginPicker.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Authentication 2 | @using Microsoft.AspNetCore.Identity 3 | @using Techtrack.Ui.Data 4 | 5 | @inject SignInManager SignInManager 6 | @inject IdentityRedirectManager RedirectManager 7 | 8 | @if (externalLogins.Length == 0) 9 | { 10 |
11 |

12 | There are no external authentication services configured. See this article 13 | about setting up this ASP.NET application to support logging in via external services. 14 |

15 |
16 | } 17 | else 18 | { 19 |
20 |
21 | 22 | 23 |

24 | @foreach (var provider in externalLogins) 25 | { 26 | 27 | } 28 |

29 |
30 |
31 | } 32 | 33 | @code { 34 | private AuthenticationScheme[] externalLogins = []; 35 | 36 | [SupplyParameterFromQuery] 37 | private string? ReturnUrl { get; set; } 38 | 39 | protected override async Task OnInitializedAsync() 40 | { 41 | externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/ManageLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout AccountLayout 3 | 4 |

Manage your account

5 | 6 |
7 |

Change your account settings

8 |
9 |
10 |
11 | 12 |
13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/ManageNavMenu.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using Techtrack.Ui.Data 3 | 4 | @inject SignInManager SignInManager 5 | 6 | 29 | 30 | @code { 31 | private bool hasExternalLogins; 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/ShowRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 |  2 |

Recovery codes

3 | 11 |
12 |
13 | @foreach (var recoveryCode in RecoveryCodes) 14 | { 15 |
16 | @recoveryCode 17 |
18 | } 19 |
20 |
21 | 22 | @code { 23 | [Parameter] 24 | public string[] RecoveryCodes { get; set; } = []; 25 | 26 | [Parameter] 27 | public string? StatusMessage { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Account/Shared/StatusMessage.razor: -------------------------------------------------------------------------------- 1 | @if (!string.IsNullOrEmpty(DisplayMessage)) 2 | { 3 | var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; 4 | 7 | } 8 | 9 | @code { 10 | private string? messageFromCookie; 11 | 12 | [Parameter] 13 | public string? Message { get; set; } 14 | 15 | [CascadingParameter] 16 | private HttpContext HttpContext { get; set; } = default!; 17 | 18 | private string? DisplayMessage => Message ?? messageFromCookie; 19 | 20 | protected override void OnInitialized() 21 | { 22 | messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; 23 | 24 | if (messageFromCookie is not null) 25 | { 26 | HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | 3 | @inject NavigationManager NavigationManager 4 | 5 | 10 | 11 | 12 | 13 | 65 | 66 | @code { 67 | private string? currentUrl; 68 | 69 | protected override void OnInitialized() 70 | { 71 | currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 72 | NavigationManager.LocationChanged += OnLocationChanged; 73 | } 74 | 75 | private void OnLocationChanged(object? sender, LocationChangedEventArgs e) 76 | { 77 | currentUrl = NavigationManager.ToBaseRelativePath(e.Location); 78 | StateHasChanged(); 79 | } 80 | 81 | public void Dispose() 82 | { 83 | NavigationManager.LocationChanged -= OnLocationChanged; 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Pages/Equipments/UpdateEquipment.razor: -------------------------------------------------------------------------------- 1 | @page "/equipment/update/{id:guid}" 2 | @using System.Text.Json 3 | @using TechTrack.Common.Interfaces.HttpClients 4 | @using TechTrack.Common.Pagination 5 | @using TechTrack.Common.Dtos.Equipments 6 | @using TechTrack.Common.ViewModel.Users 7 | @using TechTrack.Domain.Enums 8 | @inject IEquipmentsHttpClientService EquipmentsHttpClientService 9 | @inject IUserHttpClientService UsersHttpClientService 10 | @inject NavigationManager NavigationManager 11 | 12 | @rendermode InteractiveServer 13 | 14 |

Update Equipment

15 | 16 | @if (equipment == null || users == null) 17 | { 18 |

Loading...

19 | } 20 | else 21 | { 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 39 |
40 |
41 | 42 | 48 |
49 | 50 |
51 | 52 | 53 | } 54 | 55 | @code { 56 | private EquipmentDto? equipment; 57 | private List? users; 58 | [Parameter] 59 | public Guid id { get; set; } 60 | 61 | protected override async Task OnInitializedAsync() 62 | { 63 | equipment = await EquipmentsHttpClientService.GetEquipmentAsync(id); 64 | users = await UsersHttpClientService.GetUsersWithEquipmentsAsync(); 65 | } 66 | 67 | private async Task UpdateEquipments() 68 | { 69 | if (equipment != null) 70 | { 71 | var equipmentForUpdateDto = new EquipmentForUpdateDto 72 | { 73 | Name = equipment.Name, 74 | Type = equipment.Type, 75 | Status = equipment.Status, 76 | AssignedToUserId = equipment.AssignedToUserId 77 | }; 78 | 79 | await EquipmentsHttpClientService.UpdateEquipmentAsync(id, equipmentForUpdateDto); 80 | NavigationManager.NavigateTo(NavigationManager.BaseUri + "equipment", true); 81 | } 82 | } 83 | 84 | private void SeeEquipments() 85 | { 86 | NavigationManager.NavigateTo("/equipment"); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.JSInterop 10 | @using Techtrack.Ui 11 | @using Techtrack.Ui.Client 12 | @using Techtrack.Ui.Components 13 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Techtrack.Ui.Data; 5 | public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Data/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace Techtrack.Ui.Data; 4 | // Add profile data for application users by adding properties to the ApplicationUser class 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/DataSeeder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.EntityFrameworkCore; 3 | using Techtrack.Ui.Data; 4 | 5 | namespace Techtrack.Ui 6 | { 7 | public static class DataSeeder 8 | { 9 | public static async Task SeedDataAsync(IServiceProvider serviceProvider) 10 | { 11 | using var scope = serviceProvider.CreateScope(); 12 | var userManager = scope.ServiceProvider.GetRequiredService>(); 13 | 14 | // Check if there are any users in the database 15 | if (await userManager.Users.AnyAsync()) 16 | { 17 | return; // Database has been already seeded with users 18 | } 19 | 20 | // Seed users 21 | var users = new ApplicationUser[] 22 | { 23 | new() { Id = "46187ce0-5637-4435-805a-48c59ebface6", UserName = "user1@example.com", Email = "user1@example.com", PhoneNumberConfirmed = true, EmailConfirmed = true }, 24 | new() { Id = "9713413c-50a6-429e-a01b-0ed725842b1c", UserName = "user1@example.com", Email = "user2@example.com", PhoneNumberConfirmed = true, EmailConfirmed = true }, 25 | new() { Id = "c4970886-a8cb-4e2a-aab9-6231d092d913", UserName = "user1@example.com", Email = "user3@example.com", PhoneNumberConfirmed = true, EmailConfirmed = true }, 26 | }; 27 | 28 | foreach (var user in users) 29 | { 30 | await userManager.CreateAsync(user, "P@ssw0rd"); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Authorization; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.EntityFrameworkCore; 4 | using Techtrack.Ui; 5 | using Techtrack.Ui.Client.Components.Pages; 6 | using Techtrack.Ui.Client.Components.Pages.Equipments; 7 | using Techtrack.Ui.Client.Services; 8 | using Techtrack.Ui.Components; 9 | using Techtrack.Ui.Components.Account; 10 | using Techtrack.Ui.Data; 11 | using TechTrack.Common.Interfaces.HttpClients; 12 | using IUserHttpClientService = TechTrack.Common.Interfaces.HttpClients.IUserHttpClientService; 13 | 14 | var builder = WebApplication.CreateBuilder(args); 15 | 16 | // Add services to the container. 17 | builder.Services.AddRazorComponents() 18 | .AddInteractiveServerComponents() 19 | .AddInteractiveWebAssemblyComponents(); 20 | 21 | builder.Services.AddCascadingAuthenticationState(); 22 | builder.Services.AddScoped(); 23 | builder.Services.AddScoped(); 24 | builder.Services.AddScoped(); 25 | 26 | 27 | 28 | builder.Services.AddScoped(sp => 29 | { 30 | var httpClient = new HttpClient { BaseAddress = new Uri("https://localhost:7212/") }; 31 | return new UsersHttpClientService(httpClient); 32 | }); 33 | 34 | builder.Services.AddScoped(sp => 35 | { 36 | var httpClient = new HttpClient { BaseAddress = new Uri("https://localhost:7212/") }; 37 | return new EquipmentsHttpClientService(httpClient); 38 | }); 39 | 40 | builder.Services.AddAuthentication(options => 41 | { 42 | options.DefaultScheme = IdentityConstants.ApplicationScheme; 43 | options.DefaultSignInScheme = IdentityConstants.ExternalScheme; 44 | }) 45 | .AddIdentityCookies(); 46 | 47 | var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); 48 | builder.Services.AddDbContext(options => 49 | options.UseSqlServer(connectionString)); 50 | builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 51 | 52 | builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) 53 | .AddEntityFrameworkStores() 54 | .AddSignInManager() 55 | .AddDefaultTokenProviders(); 56 | 57 | builder.Services.AddSingleton, IdentityNoOpEmailSender>(); 58 | 59 | var app = builder.Build(); 60 | 61 | // Configure the HTTP request pipeline. 62 | if (app.Environment.IsDevelopment()) 63 | { 64 | app.UseWebAssemblyDebugging(); 65 | app.UseMigrationsEndPoint(); 66 | } 67 | else 68 | { 69 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 70 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 71 | app.UseHsts(); 72 | } 73 | 74 | app.UseHttpsRedirection(); 75 | 76 | app.UseStaticFiles(); 77 | app.UseAntiforgery(); 78 | 79 | app.MapRazorComponents() 80 | .AddInteractiveServerRenderMode() 81 | .AddInteractiveWebAssemblyRenderMode() 82 | .AddAdditionalAssemblies(typeof(Equipment).Assembly); 83 | 84 | // Add additional endpoints required by the Identity /Account Razor components. 85 | app.MapAdditionalIdentityEndpoints(); 86 | 87 | if (app.Environment.IsDevelopment()) 88 | { 89 | await DataSeeder.SeedDataAsync(app.Services); 90 | } 91 | 92 | app.Run(); 93 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:33316", 8 | "sslPort": 44339 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5174", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7071;http://localhost:5174", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql.local", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/Techtrack.Ui.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | aspnet-Techtrack.Ui-0187b80f-a114-4cf8-8b76-9777b5a9f88b 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "DetailedErrors": true 9 | } 10 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Techtrack.Ui-0187b80f-a114-4cf8-8b76-9777b5a9f88b;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*", 12 | "DetailedErrors": true 13 | } 14 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | -------------------------------------------------------------------------------- /Techtrack.Ui/Techtrack.Ui/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TSiustis/TechTrack/f9d507385c2e7a19a41923fa1465585b94fda95c/Techtrack.Ui/Techtrack.Ui/wwwroot/favicon.png --------------------------------------------------------------------------------