├── .gitattributes ├── .gitignore ├── BugLab.API ├── Assets │ └── email-template.html ├── BugLab.API.csproj ├── Controllers │ ├── AuthController.cs │ ├── BaseApiController.cs │ ├── BugTypesController.cs │ ├── BugsController.cs │ ├── CommentsController.cs │ ├── ProjectUsersController.cs │ ├── ProjectsController.cs │ ├── SprintsController.cs │ ├── TokenController.cs │ └── UsersController.cs ├── Extensions │ ├── HttpExtensions.cs │ ├── InputFormattersExtensions.cs │ └── ServicesExtensions.cs ├── Middlewares │ ├── DemoUserMiddleware.cs │ └── ExceptionMiddleware.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── BugLab.Blazor ├── App.razor ├── BugLab.Blazor.csproj ├── Components │ ├── Auth │ │ ├── LoginComponent.razor │ │ └── RegisterComponent.razor │ ├── BugTypes │ │ └── BugTypesComponent.razor │ ├── Bugs │ │ ├── AddBugComponent.razor │ │ ├── BugCardComponent.razor │ │ └── CommentsComponent.razor │ ├── Projects │ │ ├── AddProjectComponent.razor │ │ ├── ProjectCardComponent.razor │ │ └── ProjectMembersListComponent.razor │ ├── Sprints │ │ └── AddSprintForm.razor │ └── Users │ │ └── DashboardComponent.razor ├── Dialogs │ ├── AddProjectMemberDialog.razor │ └── UpsertCommentDialog.razor ├── Helpers │ ├── AuthState.cs │ ├── Css.cs │ ├── Endpoints.cs │ ├── HttpExtensions.cs │ ├── JsonOptions.cs │ └── TokenHelper.cs ├── Interceptors │ ├── ExceptionInterceptor.cs │ └── RefreshTokenInterceptor.cs ├── Pages │ ├── BugsList.razor │ ├── EmailConfirmation.razor │ ├── Errors │ │ ├── NotFound.razor │ │ └── ServerError.razor │ ├── Index.razor │ ├── ProjectsList.razor │ └── SprintsList.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ └── NavMenu.razor ├── _Imports.razor └── wwwroot │ ├── css │ ├── app.css │ └── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff │ ├── favicon.ico │ └── index.html ├── BugLab.Business ├── BackgroundServices │ └── RemoveDeletedBugsService.cs ├── BugLab.Business.csproj ├── CommandHandlers │ ├── Auth │ │ ├── ConfirmEmailHandler.cs │ │ ├── LoginHandler.cs │ │ ├── RefreshTokenHandler.cs │ │ ├── RegisterHandler.cs │ │ └── ResendEmailConfirmationHandler.cs │ ├── BugTypes │ │ ├── AddBugTypeHandler.cs │ │ ├── DeleteBugTypeHandler.cs │ │ └── UpdateBugTypeHandler.cs │ ├── Bugs │ │ ├── AddBugHandler.cs │ │ ├── DeleteBugHandler.cs │ │ └── UpdateBugHandler.cs │ ├── Comments │ │ ├── AddCommentHandler.cs │ │ ├── DeleteCommentHandler.cs │ │ └── UpdateCommentHandler.cs │ ├── Projects │ │ ├── AddProjectHandler.cs │ │ ├── AddProjectUsersHandler.cs │ │ ├── DeleteProjectHandler.cs │ │ ├── DeleteProjectUserHandler.cs │ │ └── UpdateProjectHandler.cs │ └── Sprints │ │ └── AddSprintHandler.cs ├── Commands │ ├── Auth │ │ ├── ConfirmEmailCommand.cs │ │ ├── LoginCommand.cs │ │ ├── RefreshTokenCommand.cs │ │ ├── RegisterCommand.cs │ │ └── ResendEmailConfirmationCommand.cs │ ├── BugTypes │ │ ├── AddBugTypeCommand.cs │ │ ├── DeleteBugTypeCommand.cs │ │ └── UpdateBugTypeCommand.cs │ ├── Bugs │ │ ├── AddBugCommand.cs │ │ ├── DeleteBugCommand.cs │ │ └── UpdateBugCommand.cs │ ├── Comments │ │ ├── AddCommentCommand.cs │ │ ├── DeleteCommentCommand.cs │ │ └── UpdateCommentCommand.cs │ ├── Projects │ │ ├── AddProjectCommand.cs │ │ ├── AddProjectUsersCommand.cs │ │ ├── DeleteProjectCommand.cs │ │ ├── DeleteProjectUserCommand.cs │ │ └── UpdateProjectCommand.cs │ └── Sprints │ │ ├── AddSprintCommand.cs │ │ └── DeleteSprintCommand.cs ├── Extensions │ ├── IQueryableExtensions.cs │ └── ServicesExtensions.cs ├── Helpers │ ├── Guard.cs │ ├── Mappings.cs │ └── PagedList.cs ├── Interfaces │ ├── IAuthService.cs │ ├── ICacheable.cs │ ├── IEmailService.cs │ └── ITokenService.cs ├── Options │ ├── ClientOptions.cs │ ├── EmailOptions.cs │ └── JwtOptions.cs ├── PipelineBehaviors │ └── CachingBehavior.cs ├── Queries │ ├── BugTypes │ │ ├── GetBugTypeQuery.cs │ │ └── GetBugTypesQuery.cs │ ├── Bugs │ │ ├── GetBugQuery.cs │ │ └── GetBugsQuery.cs │ ├── Projects │ │ ├── GetProjectQuery.cs │ │ ├── GetProjectUsersQuery.cs │ │ └── GetProjectsQuery.cs │ ├── Sprints │ │ ├── GetSprintQuery.cs │ │ └── GetSprintsQuery.cs │ └── Users │ │ ├── GetDashboardQuery.cs │ │ └── GetUsersQuery.cs ├── QueryHandlers │ ├── BugTypes │ │ ├── GetBugTypeHandler.cs │ │ └── GetBugTypesHandler.cs │ ├── Bugs │ │ ├── GetBugHandler.cs │ │ └── GetBugsHandler.cs │ ├── Projects │ │ ├── GetProjectHandler.cs │ │ ├── GetProjectUsersHandler.cs │ │ └── GetProjectsHandler.cs │ ├── Sprints │ │ ├── GetSprintHandler.cs │ │ └── GetSprintsHandler.cs │ └── Users │ │ ├── GetDashboardHandler.cs │ │ └── GetUsersHandler.cs └── Services │ ├── AuthService.cs │ ├── EmailService.cs │ └── TokenService.cs ├── BugLab.Data ├── AppDbContext.cs ├── BugLab.Data.csproj ├── Entities │ ├── AuditableEntity.cs │ ├── Bug.cs │ ├── BugType.cs │ ├── Comment.cs │ ├── Project.cs │ ├── ProjectUser.cs │ ├── RefreshToken.cs │ └── Sprint.cs ├── EntityConfigs │ ├── BugEntityConfig.cs │ ├── BugTypeEntityConfig.cs │ ├── CommentEntityConfig.cs │ ├── ProjectEntityConfig.cs │ ├── ProjectUserEntityConfig.cs │ └── SprintEntityConfig.cs ├── Extensions │ ├── ClaimsPrincipalExtensions.cs │ ├── EntityConfigurationExtensions.cs │ ├── EntityEntryExtensions.cs │ ├── ModelBuilderExtensions.cs │ └── ServicesExtensions.cs ├── Helpers │ ├── DateProvider.cs │ └── IDateProvider.cs └── Migrations │ ├── 20220404202356_SetNull.Designer.cs │ ├── 20220404202356_SetNull.cs │ └── AppDbContextModelSnapshot.cs ├── BugLab.Shared ├── BugLab.Shared.csproj ├── Enums │ ├── BugEnums.cs │ └── SortOrder.cs ├── Headers │ └── PaginationHeader.cs ├── Helpers │ └── HttpClientHelpers │ │ ├── IKeyValueBuilder.cs │ │ └── QueryBuilder.cs ├── QueryParams │ ├── BugParams.cs │ ├── PaginationParams.cs │ └── UserParams.cs ├── Requests │ ├── Auth │ │ ├── LoginRequest.cs │ │ ├── RefreshTokenRequest.cs │ │ └── RegisterRequest.cs │ ├── BugTypes │ │ └── UpsertBugTypeRequest.cs │ ├── Bugs │ │ ├── AddBugRequest.cs │ │ └── UpdateBugRequest.cs │ ├── Comments │ │ └── UpsertCommentRequest.cs │ ├── Projects │ │ ├── AddProjectRequest.cs │ │ └── UpdateProjectRequest.cs │ └── Sprints │ │ └── AddSprintRequest.cs ├── Responses │ ├── ApiError.cs │ ├── BugResponse.cs │ ├── BugTypeResponse.cs │ ├── CommentResponse.cs │ ├── DashboardResponse.cs │ ├── LoginResponse.cs │ ├── ProjectResponse.cs │ ├── SprintDetailsResponse.cs │ ├── SprintForListResponse.cs │ └── UserResponse.cs └── Validators │ ├── AddBugValidator.cs │ ├── AddProjectValidator.cs │ ├── AddSprintValidator.cs │ ├── LoginValidator.cs │ ├── RegisterValidator.cs │ ├── UpdateBugValidator.cs │ ├── UpdateProjectValidator.cs │ ├── UpsertBugTypeValidator.cs │ └── UpsertCommentValidator.cs ├── BugLab.Tests ├── BugLab.Tests.csproj ├── Business │ ├── CommandHandlers │ │ ├── AddBugHandlerTests.cs │ │ ├── AddCommentHandlerTests.cs │ │ ├── AddProjectUsersHandlerTests.cs │ │ ├── DeleteBugHandlerTests.cs │ │ ├── DeleteBugTypeHandlerTests.cs │ │ ├── DeleteProjectHandlerTests.cs │ │ └── DeleteProjectUserHandlerTests.cs │ ├── Extensions │ │ └── IQueryableExtensionsTests.cs │ ├── QueryHandlers │ │ ├── GetBugsHandlerTests.cs │ │ └── GetProjectHandlerTests.cs │ └── Services │ │ └── AuthServiceTests.cs ├── Helpers │ ├── DbContextHelpers.cs │ └── Seed.cs └── Shared │ └── QueryBuilderTests.cs ├── BugLab.sln └── README.md /.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 | -------------------------------------------------------------------------------- /BugLab.API/Assets/email-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 |

Welcome to BugLab

14 | Click here to confirm your email 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /BugLab.API/BugLab.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | a641eda6-2f11-49c2-928d-394f9d80dfe0 6 | 7 | 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 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Shared.Requests.Auth; 3 | using BugLab.Shared.Responses; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.API.Controllers 12 | { 13 | [AllowAnonymous] 14 | public class AuthController : BaseApiController 15 | { 16 | public AuthController(IMediator mediator) : base(mediator) 17 | { 18 | } 19 | 20 | [HttpPost("login")] 21 | public async Task> Login(LoginRequest request, CancellationToken cancellationToken) 22 | { 23 | var user = await _mediator.Send(request.Adapt(), cancellationToken); 24 | 25 | return user; 26 | } 27 | 28 | [HttpPost("register")] 29 | public async Task Register(RegisterRequest request, CancellationToken cancellationToken) 30 | { 31 | await _mediator.Send(request.Adapt(), cancellationToken); 32 | 33 | return NoContent(); 34 | } 35 | 36 | [HttpPost("{userId}/resend-confirm-email")] 37 | public async Task ResendConfirmEmail(string userId) 38 | { 39 | await _mediator.Send(new ResendEmailConfirmationCommand(userId)); 40 | 41 | return NoContent(); 42 | } 43 | 44 | [HttpPost("{userId}/confirm-email")] 45 | public async Task ConfirmEmail([FromQuery] string token, string userId) 46 | { 47 | if (string.IsNullOrWhiteSpace(token)) return BadRequest("Invalid Confirmation Token"); 48 | 49 | await _mediator.Send(new ConfirmEmailCommand(userId, token)); 50 | 51 | return NoContent(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/BaseApiController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BugLab.API.Controllers 6 | { 7 | [ApiController] 8 | [Authorize] 9 | [Route("api/[controller]")] 10 | public abstract class BaseApiController : ControllerBase 11 | { 12 | protected readonly IMediator _mediator; 13 | 14 | public BaseApiController(IMediator mediator) 15 | { 16 | _mediator = mediator; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/BugTypesController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.BugTypes; 2 | using BugLab.Business.Interfaces; 3 | using BugLab.Business.Queries.BugTypes; 4 | using BugLab.Data.Extensions; 5 | using BugLab.Shared.Requests.BugTypes; 6 | using BugLab.Shared.Responses; 7 | using MediatR; 8 | using Microsoft.AspNetCore.Mvc; 9 | using System.Collections.Generic; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.API.Controllers 14 | { 15 | [Route("api/projects/{projectId}/[controller]")] 16 | public class BugTypesController : BaseApiController 17 | { 18 | private readonly IAuthService _authService; 19 | 20 | public BugTypesController(IMediator mediator, IAuthService authService) : base(mediator) 21 | { 22 | _authService = authService; 23 | } 24 | 25 | [HttpGet] 26 | public async Task>> GetBugTypes(int projectId, CancellationToken cancellationToken) 27 | { 28 | var bugTypes = await _mediator.Send(new GetBugTypesQuery(projectId), cancellationToken); 29 | 30 | return Ok(bugTypes); 31 | } 32 | 33 | [HttpPost] 34 | public async Task AddBugType(int projectId, UpsertBugTypeRequest request, CancellationToken cancellationToken) 35 | { 36 | await _authService.HasAccessToProject(User.UserId(), projectId); 37 | var command = new AddBugTypeCommand(projectId, request.Color, request.Title); 38 | var id = await _mediator.Send(command, cancellationToken); 39 | 40 | return CreatedAtRoute(nameof(GetBugType), new { projectId, id }, id); 41 | } 42 | 43 | [HttpPut("{id}")] 44 | public async Task UpdateBugType(int projectId, int id, UpsertBugTypeRequest request, CancellationToken cancellationToken) 45 | { 46 | await _authService.HasAccessToProject(User.UserId(), projectId); 47 | var command = new UpdateBugTypeCommand(id, request.Title, request.Color); 48 | await _mediator.Send(command, cancellationToken); 49 | 50 | return NoContent(); 51 | } 52 | 53 | [HttpDelete("{id}")] 54 | public async Task DeleteBugType(int projectId, int id, CancellationToken cancellationToken) 55 | { 56 | await _authService.HasAccessToProject(User.UserId(), projectId); 57 | await _mediator.Send(new DeleteBugTypeCommand(id), cancellationToken); 58 | 59 | return NoContent(); 60 | } 61 | 62 | [HttpGet("{id}", Name = nameof(GetBugType))] 63 | public async Task> GetBugType(int id, CancellationToken cancellationToken) 64 | { 65 | var bugType = await _mediator.Send(new GetBugTypeQuery(id), cancellationToken); 66 | 67 | return bugType; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/CommentsController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Comments; 2 | using BugLab.Business.Interfaces; 3 | using BugLab.Data.Extensions; 4 | using BugLab.Shared.Requests.Comments; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.API.Controllers 11 | { 12 | [Route("api/bugs/{bugId}/[controller]")] 13 | public class CommentsController : BaseApiController 14 | { 15 | private readonly IAuthService _authService; 16 | 17 | public CommentsController(IMediator mediator, IAuthService authService) : base(mediator) 18 | { 19 | _authService = authService; 20 | } 21 | 22 | [HttpPost] 23 | public async Task AddComment(int bugId, UpsertCommentRequest request, CancellationToken cancellationToken) 24 | { 25 | await _authService.HasAccessToBug(User.UserId(), bugId); 26 | await _mediator.Send(new AddCommentCommand(bugId, request.Text), cancellationToken); 27 | 28 | return CreatedAtRoute(nameof(BugsController.GetBug), new { id = bugId }, new { bugId }); 29 | } 30 | 31 | [HttpPut("{id}")] 32 | public async Task UpdateComment(int bugId, int id, UpsertCommentRequest request, CancellationToken cancellationToken) 33 | { 34 | await _authService.HasAccessToBug(User.UserId(), bugId); 35 | await _mediator.Send(new UpdateCommentCommand(id, bugId, request.Text), cancellationToken); 36 | 37 | return NoContent(); 38 | } 39 | 40 | [HttpDelete("{id}")] 41 | public async Task DeleteComment(int bugId, int id, CancellationToken cancellationToken) 42 | { 43 | await _authService.HasAccessToBug(User.UserId(), bugId); 44 | await _mediator.Send(new DeleteCommentCommand(id, bugId), cancellationToken); 45 | 46 | return NoContent(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/ProjectUsersController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Business.Interfaces; 3 | using BugLab.Business.Queries.Projects; 4 | using BugLab.Data.Extensions; 5 | using BugLab.Shared.Responses; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.API.Controllers 14 | { 15 | [Route("api/projects/{projectId}/[controller]")] 16 | public class ProjectUsersController : BaseApiController 17 | { 18 | private readonly IAuthService _authService; 19 | 20 | public ProjectUsersController(IMediator mediator, IAuthService authService) : base(mediator) 21 | { 22 | _authService = authService; 23 | } 24 | 25 | [HttpGet] 26 | public async Task>> GetProjectUsers(int projectId, CancellationToken cancellationToken) 27 | { 28 | var users = await _mediator.Send(new GetProjectUsersQuery(projectId), cancellationToken); 29 | 30 | return Ok(users); 31 | } 32 | 33 | [HttpPost] 34 | public async Task AddUsersToProject(int projectId, [FromQuery] IEnumerable userIds, CancellationToken cancellationToken) 35 | { 36 | if (!userIds.Any()) return NoContent(); 37 | 38 | await _authService.HasAccessToProject(User.UserId(), projectId); 39 | await _mediator.Send(new AddProjectUsersCommand(projectId, userIds), cancellationToken); 40 | 41 | return NoContent(); 42 | } 43 | 44 | [HttpDelete("{id}")] 45 | public async Task DeleteProjectUser(int projectId, string id, CancellationToken cancellationToken) 46 | { 47 | await _authService.HasAccessToProject(User.UserId(), projectId); 48 | await _mediator.Send(new DeleteProjectUserCommand(projectId, id), cancellationToken); 49 | 50 | return NoContent(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/ProjectsController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.API.Extensions; 2 | using BugLab.Business.Commands.Projects; 3 | using BugLab.Business.Interfaces; 4 | using BugLab.Business.Queries.Projects; 5 | using BugLab.Data.Extensions; 6 | using BugLab.Shared.QueryParams; 7 | using BugLab.Shared.Requests.Projects; 8 | using BugLab.Shared.Responses; 9 | using Mapster; 10 | using MediatR; 11 | using Microsoft.AspNetCore.Mvc; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace BugLab.API.Controllers 18 | { 19 | public class ProjectsController : BaseApiController 20 | { 21 | private readonly IAuthService _authService; 22 | 23 | public ProjectsController(IMediator mediator, IAuthService authService) : base(mediator) 24 | { 25 | _authService = authService; 26 | } 27 | 28 | [HttpGet("{id}", Name = nameof(GetProject))] 29 | public async Task> GetProject(int id, CancellationToken cancellationToken) 30 | { 31 | var project = await _mediator.Send(new GetProjectQuery(id), cancellationToken); 32 | 33 | return project; 34 | } 35 | 36 | [HttpGet] 37 | public async Task>> GetProjects([FromQuery] PaginationParams queryParams, CancellationToken cancellationToken) 38 | { 39 | var query = new GetProjectsQuery(User.UserId()); 40 | queryParams.Adapt(query); 41 | var projects = await _mediator.Send(query, cancellationToken); 42 | Response.AddPaginationHeader(projects.PageNumber, projects.PageSize, projects.TotalPages, projects.TotalItems); 43 | 44 | return projects; 45 | } 46 | 47 | [HttpPost] 48 | public async Task AddProject(AddProjectRequest request, CancellationToken cancellationToken) 49 | { 50 | var id = await _mediator.Send(new AddProjectCommand(User.UserId(), request.Title, request.Description), cancellationToken); 51 | 52 | return CreatedAtRoute(nameof(GetProject), new { id }, id); 53 | } 54 | 55 | [HttpPut("{id}")] 56 | public async Task UpdateProject(int id, UpdateProjectRequest request, CancellationToken cancellationToken) 57 | { 58 | await _authService.HasAccessToProject(User.UserId(), id); 59 | await _mediator.Send(new UpdateProjectCommand(id, request.Title, request.Description), cancellationToken); 60 | 61 | return NoContent(); 62 | } 63 | 64 | [HttpDelete("{id}")] 65 | public async Task DeleteProject(int id, CancellationToken cancellationToken) 66 | { 67 | await _authService.HasAccessToProject(User.UserId(), id); 68 | await _mediator.Send(new DeleteProjectCommand(id), cancellationToken); 69 | 70 | return NoContent(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /BugLab.API/Controllers/SprintsController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Sprints; 2 | using BugLab.Business.Interfaces; 3 | using BugLab.Business.Queries.Sprints; 4 | using BugLab.Data.Extensions; 5 | using BugLab.Shared.Requests.Sprints; 6 | using BugLab.Shared.Responses; 7 | using MediatR; 8 | using Microsoft.AspNetCore.Mvc; 9 | using System.Collections.Generic; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.API.Controllers 14 | { 15 | [Route("api/projects/{projectId}/[controller]")] 16 | public class SprintsController : BaseApiController 17 | { 18 | private readonly IAuthService _authService; 19 | 20 | public SprintsController(IMediator mediator, IAuthService authService) : base(mediator) 21 | { 22 | _authService = authService; 23 | } 24 | 25 | [HttpGet] 26 | public async Task>> GetSprints(int projectId, CancellationToken cancellationToken) 27 | { 28 | var sprints = await _mediator.Send(new GetSprintsQuery(projectId), cancellationToken); 29 | 30 | return Ok(sprints); 31 | } 32 | 33 | [HttpGet("{id}", Name = nameof(GetSprint))] 34 | public async Task> GetSprint(int id, CancellationToken cancellationToken) 35 | { 36 | var sprint = await _mediator.Send(new GetSprintQuery(id), cancellationToken); 37 | 38 | return Ok(sprint); 39 | } 40 | 41 | [HttpDelete("{id}")] 42 | public async Task> DeleteSprint(int projectId, int id, CancellationToken cancellationToken) 43 | { 44 | await _authService.HasAccessToProject(User.UserId(), projectId); 45 | await _mediator.Send(new DeleteSprintCommand(id), cancellationToken); 46 | 47 | return NoContent(); 48 | } 49 | 50 | [HttpPost] 51 | public async Task AddSprint(int projectId, AddSprintRequest request, CancellationToken cancellationToken) 52 | { 53 | await _authService.HasAccessToProject(User.UserId(), projectId); 54 | var command = new AddSprintCommand(projectId, request.Title, request.StartDate, request.EndDate); 55 | var id = await _mediator.Send(command, cancellationToken); 56 | 57 | return CreatedAtRoute(nameof(GetSprint), new { projectId, id }, id); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/TokenController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Shared.Requests.Auth; 3 | using Mapster; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.Threading.Tasks; 7 | 8 | namespace BugLab.API.Controllers 9 | { 10 | public class TokenController : BaseApiController 11 | { 12 | public TokenController(IMediator mediator) : base(mediator) 13 | { 14 | } 15 | 16 | [HttpPost] 17 | public async Task> RefreshToken(RefreshTokenRequest request) 18 | { 19 | var accessToken = await _mediator.Send(request.Adapt()); 20 | return Ok(accessToken); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BugLab.API/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using BugLab.API.Extensions; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Business.Queries.Users; 4 | using BugLab.Data.Extensions; 5 | using BugLab.Shared.QueryParams; 6 | using BugLab.Shared.Responses; 7 | using Mapster; 8 | using MediatR; 9 | using Microsoft.AspNetCore.Mvc; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.API.Controllers 14 | { 15 | public class UsersController : BaseApiController 16 | { 17 | public UsersController(IMediator mediator) : base(mediator) 18 | { 19 | } 20 | 21 | [HttpGet] 22 | public async Task>> GetUsers([FromQuery] UserParams queryParams, CancellationToken cancellationToken) 23 | { 24 | var users = await _mediator.Send(queryParams.Adapt(), cancellationToken); 25 | Response.AddPaginationHeader(users.PageNumber, users.PageSize, users.TotalPages, users.TotalItems); 26 | 27 | return users; 28 | } 29 | 30 | [HttpGet("dashboard")] 31 | public async Task> GetDashboard(CancellationToken cancellationToken) 32 | { 33 | var dashboard = await _mediator.Send(new GetDashboardQuery(User.UserId()), cancellationToken); 34 | 35 | return dashboard; 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BugLab.API/Extensions/HttpExtensions.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Headers; 2 | using Microsoft.AspNetCore.Http; 3 | using System.Text.Json; 4 | 5 | namespace BugLab.API.Extensions 6 | { 7 | public static class HttpExtensions 8 | { 9 | public static void AddPaginationHeader(this HttpResponse response, int pageNumber, int pageSize, int totalPages, int totalItems) 10 | { 11 | var paginationHeader = new PaginationHeader(pageNumber, pageSize, totalItems, totalPages); 12 | var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; 13 | 14 | response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, jsonOptions)); 15 | response.Headers.Add("Access-Control-Expose-Headers", "Pagination"); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /BugLab.API/Extensions/InputFormattersExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Formatters; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Options; 5 | using System.Linq; 6 | 7 | namespace BugLab.API.Extensions 8 | { 9 | public static class InputFormattersExtensions 10 | { 11 | public static FormatterCollection InsertJsonPatch(this FormatterCollection formatters) 12 | { 13 | var builder = new ServiceCollection() 14 | .AddLogging() 15 | .AddMvc() 16 | .AddNewtonsoftJson() 17 | .Services.BuildServiceProvider(); 18 | 19 | var formatter = builder 20 | .GetRequiredService>() 21 | .Value 22 | .InputFormatters 23 | .OfType() 24 | .First(); 25 | 26 | formatters.Insert(0, formatter); 27 | return formatters; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.API/Middlewares/DemoUserMiddleware.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Extensions; 2 | using BugLab.Shared.Responses; 3 | using Microsoft.AspNetCore.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace BugLab.API.Middlewares 7 | { 8 | public class DemoUserMiddleware 9 | { 10 | private readonly RequestDelegate _next; 11 | 12 | public DemoUserMiddleware(RequestDelegate next) 13 | { 14 | _next = next; 15 | } 16 | 17 | public async Task InvokeAsync(HttpContext context) 18 | { 19 | var requestType = context.Request.Method; 20 | var userId = context.User.UserId(); 21 | 22 | if (requestType == "GET" || string.IsNullOrWhiteSpace(userId) || userId != "757b2158-40c3-4917-9523-5861973a4d2e") 23 | { 24 | await _next(context); 25 | return; 26 | } 27 | 28 | context.Response.StatusCode = StatusCodes.Status403Forbidden; 29 | var response = new ApiError($"You are not allowed to make a {requestType} request using the demo user"); 30 | await context.Response.WriteAsJsonAsync(response); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BugLab.API/Middlewares/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.IdentityModel.Tokens; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text.Json; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.API.Middlewares 13 | { 14 | public class ExceptionMiddleware 15 | { 16 | private readonly RequestDelegate _next; 17 | private readonly ILogger _logger; 18 | private readonly IWebHostEnvironment _env; 19 | 20 | public ExceptionMiddleware(RequestDelegate next, ILogger logger, IWebHostEnvironment env) 21 | { 22 | _next = next; 23 | _logger = logger; 24 | _env = env; 25 | } 26 | 27 | public async Task InvokeAsync(HttpContext context) 28 | { 29 | try 30 | { 31 | await _next(context); 32 | } 33 | catch (Exception ex) 34 | { 35 | _logger.LogError(ex, ex.Message); 36 | 37 | context.Response.StatusCode = ex switch 38 | { 39 | KeyNotFoundException => StatusCodes.Status404NotFound, 40 | InvalidOperationException or SecurityTokenException => StatusCodes.Status400BadRequest, 41 | UnauthorizedAccessException => StatusCodes.Status403Forbidden, 42 | _ => StatusCodes.Status500InternalServerError 43 | }; 44 | 45 | var result = JsonSerializer.Serialize(_env.IsDevelopment() 46 | ? new ApiError(ex.Message, ex?.StackTrace) 47 | : new ApiError(ex.Message)); 48 | 49 | await context.Response.WriteAsync(result); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BugLab.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Serilog; 4 | using Serilog.Events; 5 | using Serilog.Sinks.SystemConsole.Themes; 6 | 7 | namespace BugLab.API 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | ConfigureLogger(); 14 | CreateHostBuilder(args).Build().Run(); 15 | } 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args) => 18 | Host.CreateDefaultBuilder(args) 19 | .UseSerilog() 20 | .ConfigureWebHostDefaults(webBuilder => 21 | { 22 | webBuilder.UseStartup(); 23 | }); 24 | 25 | public static void ConfigureLogger() 26 | { 27 | Log.Logger = new LoggerConfiguration() 28 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 29 | .WriteTo.Console(theme: AnsiConsoleTheme.Code) 30 | .CreateLogger(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /BugLab.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:43493", 8 | "sslPort": 44365 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "BugLab.API": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /BugLab.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using BugLab.API.Extensions; 2 | using BugLab.API.Middlewares; 3 | using BugLab.Shared.Validators; 4 | using FluentValidation.AspNetCore; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using System.Text.Json.Serialization; 11 | 12 | namespace BugLab.API 13 | { 14 | public class Startup 15 | { 16 | private readonly IConfiguration _config; 17 | private readonly IWebHostEnvironment _environment; 18 | 19 | public Startup(IConfiguration configuration, IWebHostEnvironment environment) 20 | { 21 | _config = configuration; 22 | _environment = environment; 23 | } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddApiServices(_config, _environment); 29 | 30 | services.AddControllers(opt => opt.InputFormatters.InsertJsonPatch()) 31 | .AddJsonOptions(opt => opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())) 32 | .AddFluentValidation(opt => opt.RegisterValidatorsFromAssemblyContaining()); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 37 | { 38 | if (env.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | app.UseSwagger(); 42 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "BugLab.API v1")); 43 | } 44 | 45 | app.UseMiddleware(); 46 | 47 | app.UseHttpsRedirection(); 48 | app.UseCors(); 49 | app.UseRouting(); 50 | 51 | app.UseAuthentication(); 52 | app.UseAuthorization(); 53 | 54 | if (!env.IsDevelopment()) app.UseMiddleware(); 55 | 56 | app.UseEndpoints(endpoints => 57 | { 58 | endpoints.MapControllers(); 59 | }); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /BugLab.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "Default": "Server=(localdb)\\mssqllocaldb;Database=BugLab;Trusted_Connection=True;MultipleActiveResultSets=true" 11 | }, 12 | "ClientOptions": { 13 | "Uri": "https://localhost:4201" 14 | }, 15 | "JwtOptions": { 16 | "TokenKey": "thisIsASuperSecretToken21312", 17 | "ValidIssuer": "https://localhost:5001", 18 | "ValidAudience": "https://localhost:4201" 19 | } 20 | } -------------------------------------------------------------------------------- /BugLab.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } -------------------------------------------------------------------------------- /BugLab.Blazor/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Making sure you are logged in... 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Sorry, there's nothing at this address.

13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /BugLab.Blazor/BugLab.Blazor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | portable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Auth/LoginComponent.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | @inject AuthState AuthState 3 | @inject NavigationManager Nav 4 | @inject ISnackbar Snackbar 5 | 6 | 7 | 8 | 10 | 11 | 13 | 14 | Login 15 | Login with sample user 16 | 17 | 18 | @code{ 19 | private LoginRequest _model = new(); 20 | private LoginValidator _validator = new(); 21 | 22 | public async Task OnSubmit() 23 | { 24 | var response = await Client.PostAsJsonAsync($"{Endpoints.Auth}/login", _model); 25 | 26 | var user = await response.Content.ReadFromJsonAsync(); 27 | if (!user.EmailConfirmed) 28 | { 29 | Snackbar.Add("Confirm your email! Click here to resend the confirmation mail", Severity.Warning, opt => 30 | { 31 | opt.VisibleStateDuration = 10000; 32 | opt.Onclick = async (e) => 33 | { 34 | await Client.PostAsync($"{Endpoints.Auth}/{user.Id}/resend-confirm-email", null); 35 | }; 36 | }); 37 | } 38 | 39 | await AuthState.LogInAsync(user); 40 | Nav.NavigateTo("/"); 41 | } 42 | 43 | public async Task SampleUserLogin() 44 | { 45 | _model.Email = "chris@gmail.com"; 46 | _model.Password = "Password123."; 47 | await OnSubmit(); 48 | } 49 | } -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Auth/RegisterComponent.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | @inject ISnackbar Snackbar 3 | 4 | 5 | 6 | 8 | 9 | 11 | 12 | Register 13 | 14 | 15 | @code{ 16 | private RegisterRequest _model = new(); 17 | private RegisterValidator _validator = new(); 18 | 19 | public async Task OnSubmit() 20 | { 21 | await Client.PostAsJsonAsync($"{Endpoints.Auth}/register", _model); 22 | Snackbar.Add("Check your inbox to confirm your email", Severity.Success); 23 | _model = new(); 24 | } 25 | } -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Projects/AddProjectComponent.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | 3 | 4 | 5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @code { 19 | private AddProjectRequest _model = new(); 20 | 21 | [Parameter] public EventCallback OnAdd { get; set; } 22 | 23 | public async Task OnSubmit() 24 | { 25 | var response = await Client.PostAsJsonAsync(Endpoints.Projects, _model); 26 | var id = await response.Content.ReadFromJsonAsync(); 27 | 28 | await OnAdd.InvokeAsync(id); 29 | } 30 | } 31 | 32 | 37 | -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Projects/ProjectCardComponent.razor: -------------------------------------------------------------------------------- 1 | @inject IDialogService Dialog 2 | @inject HttpClient Client 3 | @inject ISnackbar Toastr 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | Total high prioritized bugs: @Project.TotalHighPriorityBugs 18 | Total bugs: @Project.TotalBugs 19 | 20 | 21 | 22 | 23 | @if (context.IsModified()) 24 | { 25 | You have unsaved changes 26 | 27 | } 28 | 29 | 30 | 31 | 32 | @code { 33 | 34 | private UpdateProjectRequest _model; 35 | private UpdateProjectValidator _validator = new(); 36 | 37 | [Parameter] public ProjectResponse Project { get; set; } 38 | [Parameter] public EventCallback OnDelete { get; set; } 39 | [Parameter] public EventCallback OnUpdate { get; set; } 40 | 41 | protected override void OnParametersSet() 42 | { 43 | _model = Project.Adapt(); 44 | } 45 | 46 | private async Task ConfirmDelete() 47 | { 48 | if (Project.TotalBugs != 0) 49 | { 50 | Toastr.Add($"delete or resolve any existing bugs before deleting {Project.Title}", Severity.Error); 51 | return; 52 | } 53 | 54 | var result = await Dialog.ShowMessageBox($"Are you sure you want to delete {Project.Title}", "This cannot be undone", "OK", "Cancel"); 55 | if (!result.HasValue || !result.Value) return; 56 | 57 | var response = await Client.DeleteAsync($"{Endpoints.Projects}/{Project.Id}"); 58 | await OnDelete.InvokeAsync(); 59 | } 60 | 61 | private async Task OnSubmit() 62 | { 63 | await Client.PutAsJsonAsync($"{Endpoints.Projects}/{Project.Id}", _model); 64 | 65 | _model.Adapt(Project); 66 | await OnUpdate.InvokeAsync(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Projects/ProjectMembersListComponent.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | @inject ISnackbar Snackbar 3 | 4 | 5 | @Members.Count() Members 6 | 7 | @foreach (var member in Members) 8 | { 9 | 10 | 11 | @member.Email 12 | 13 | 14 | Delete 15 | 16 | 17 | } 18 | 19 | 20 | 21 | @code{ 22 | [Parameter] public IEnumerable Members { get; set; } 23 | [Parameter] public int ProjectId { get; set; } 24 | 25 | public async Task RemoveMember(string id) 26 | { 27 | if (Members.Count() == 1) { Snackbar.Add("Removing the last user on the project gives no one access to it", Severity.Error); return; } 28 | 29 | await Client.DeleteAsync($"{Endpoints.ProjectUsers(ProjectId)}/{id}"); 30 | Members = Members.Where(u => u.Id != id).ToList(); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /BugLab.Blazor/Components/Sprints/AddSprintForm.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @code{ 19 | private DateRange _dateRange = new DateRange(DateTime.Now, DateTime.Now.AddDays(14)); 20 | private AddSprintRequest _model = new(); 21 | 22 | [Parameter] 23 | public int ProjectId { get; set; } 24 | [Parameter] 25 | public EventCallback OnAdd { get; set; } 26 | 27 | public async Task OnSubmit() 28 | { 29 | if (_dateRange.Start is null || _dateRange.End is null) return; 30 | _model.StartDate = _dateRange.Start.Value.ToUniversalTime(); 31 | _model.EndDate = _dateRange.End.Value.ToUniversalTime(); 32 | 33 | var response = await Client.PostAsJsonAsync(Endpoints.Sprints(ProjectId), _model); 34 | var id = await response.Content.ReadFromJsonAsync(); 35 | 36 | await OnAdd.InvokeAsync(new SprintForListResponse 37 | { 38 | Id = id, 39 | EndDate = _model.EndDate, 40 | StartDate = _model.StartDate, 41 | Title = _model.Title, 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BugLab.Blazor/Dialogs/UpsertCommentDialog.razor: -------------------------------------------------------------------------------- 1 | @inject HttpClient Client 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | @if (Comment == null) 11 | { 12 | 14 | } 15 | else 16 | { 17 | 18 | } 19 | 20 | 21 | @code{ 22 | private UpsertCommentRequest _model = new(); 23 | private UpsertCommentValidator _validator = new(); 24 | 25 | protected override void OnParametersSet() 26 | { 27 | if (Comment != null) _model.Text = Comment.Text; 28 | } 29 | 30 | [CascadingParameter] public MudDialogInstance MudDialog { get; set; } 31 | [Parameter] public int BugId { get; set; } 32 | [Parameter] public CommentResponse Comment { get; set; } 33 | 34 | public async Task OnSubmit() 35 | { 36 | if (Comment == null) await Client.PostAsJsonAsync(Endpoints.Comments(BugId), _model); 37 | else await Client.PutAsJsonAsync($"{Endpoints.Comments(BugId)}/{Comment.Id}", _model); 38 | 39 | MudDialog.Close(); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/AuthState.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using BugLab.Shared.Responses; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Components.Authorization; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Security.Claims; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Blazor.Helpers 11 | { 12 | public class AuthState : AuthenticationStateProvider 13 | { 14 | private readonly HttpClient _client; 15 | private readonly ILocalStorageService _localStorage; 16 | private readonly NavigationManager _nav; 17 | private readonly string _userKey = "user"; 18 | private readonly AuthenticationState _anonymous = new(new ClaimsPrincipal()); 19 | 20 | public AuthState(HttpClient client, ILocalStorageService localStorage, NavigationManager Nav) 21 | { 22 | _client = client; 23 | _localStorage = localStorage; 24 | _nav = Nav; 25 | } 26 | 27 | public override async Task GetAuthenticationStateAsync() 28 | { 29 | var user = await _localStorage.GetItemAsync(_userKey); 30 | if (string.IsNullOrWhiteSpace(user?.Token) || TokenHelper.ParseClaims(user.Token).HasExpired()) 31 | { 32 | return _anonymous; 33 | } 34 | 35 | _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", user.Token); 36 | 37 | return CreateAuthenticationState(user); 38 | } 39 | 40 | public async Task LogoutAsync() 41 | { 42 | await _localStorage.RemoveItemAsync(_userKey); 43 | var authState = Task.FromResult(_anonymous); 44 | _client.DefaultRequestHeaders.Authorization = null; 45 | 46 | NotifyAuthenticationStateChanged(authState); 47 | _nav.NavigateTo("/home"); 48 | } 49 | 50 | public async Task LogInAsync(LoginResponse user) 51 | { 52 | var state = CreateAuthenticationState(user); 53 | var authStateTask = Task.FromResult(state); 54 | await _localStorage.SetItemAsync(_userKey, user); 55 | _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", user.Token); 56 | 57 | NotifyAuthenticationStateChanged(authStateTask); 58 | } 59 | 60 | private AuthenticationState CreateAuthenticationState(LoginResponse user) 61 | { 62 | return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(new[] 63 | { 64 | new Claim(ClaimTypes.Email, user.Email) 65 | }, "Bearer"))); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/Css.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Blazor.Helpers 2 | { 3 | public static class Css 4 | { 5 | public const string BgColor = "background-color:"; 6 | public const string Color = "color:"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/Endpoints.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Blazor.Helpers 2 | { 3 | public static class Endpoints 4 | { 5 | public const string Auth = "/api/auth"; 6 | public const string Bugs = "/api/bugs"; 7 | public const string Projects = "/api/projects"; 8 | public const string Token = "/api/token"; 9 | public const string Users = "/api/users"; 10 | 11 | public static string BugTypes(int projectId) => $"{Projects}/{projectId}/bugTypes"; 12 | public static string Sprints(int projectId) => $"{Projects}/{projectId}/sprints"; 13 | public static string Comments(int bugId) => $"{Bugs}/{bugId}/comments"; 14 | public static string ProjectUsers(int projectId) => $"{Projects}/{projectId}/projectUsers"; 15 | } 16 | } -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/HttpExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.JsonPatch; 2 | using Newtonsoft.Json; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BugLab.Blazor.Helpers; 9 | public static class HttpExtensions 10 | { 11 | public static async Task NewtonsoftPatchAsync(this HttpClient client, string requestUri, JsonPatchDocument patchDocument) 12 | where T : class 13 | { 14 | var writer = new StringWriter(); 15 | 16 | new JsonSerializer().Serialize(writer, patchDocument); 17 | var json = writer.ToString(); 18 | 19 | var content = new StringContent(json, Encoding.UTF8, "application/json-patch+json"); 20 | return await client.PatchAsync(requestUri, content); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/JsonOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace BugLab.Blazor.Helpers 5 | { 6 | public static class JsonOptions 7 | { 8 | public static JsonSerializerOptions Defaults() 9 | { 10 | var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); 11 | options.Converters.Add(new JsonStringEnumConverter()); 12 | return options; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Blazor/Helpers/TokenHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Text.Json; 6 | 7 | namespace BugLab.Blazor.Helpers 8 | { 9 | public static class TokenHelper 10 | { 11 | public static IEnumerable ParseClaims(string token) 12 | { 13 | var payload = token.Split('.')[1]; 14 | var jsonBytes = ParseBase64WithoutPadding(payload); 15 | var claimValues = JsonSerializer.Deserialize>(jsonBytes); 16 | return claimValues.Select(cv => new Claim(cv.Key, cv.Value.ToString())); 17 | } 18 | 19 | public static bool HasExpired(this IEnumerable claims) 20 | { 21 | var expirationDate = GetExpirationDate(claims); 22 | 23 | return expirationDate <= DateTime.UtcNow; 24 | } 25 | 26 | public static DateTime GetExpirationDate(this IEnumerable claims) 27 | { 28 | var expirationClaim = claims.First(x => x.Type == "exp"); 29 | return DateTimeOffset.FromUnixTimeSeconds(long.Parse(expirationClaim.Value)).UtcDateTime; 30 | } 31 | 32 | private static byte[] ParseBase64WithoutPadding(string base64) 33 | { 34 | switch (base64.Length % 4) 35 | { 36 | case 2: base64 += "=="; break; 37 | case 3: base64 += "="; break; 38 | } 39 | 40 | return Convert.FromBase64String(base64); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BugLab.Blazor/Interceptors/ExceptionInterceptor.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | using System; 5 | using System.Net; 6 | using System.Net.Http.Json; 7 | using System.Threading.Tasks; 8 | using Toolbelt.Blazor; 9 | 10 | namespace BugLab.Blazor.Interceptors 11 | { 12 | public class ExceptionInterceptor : IDisposable 13 | { 14 | private readonly IHttpClientInterceptor _interceptor; 15 | private readonly ISnackbar _snackbar; 16 | private readonly NavigationManager _nav; 17 | 18 | public ExceptionInterceptor(IHttpClientInterceptor Interceptor, ISnackbar Snackbar, NavigationManager Nav) 19 | { 20 | _interceptor = Interceptor; 21 | _snackbar = Snackbar; 22 | _nav = Nav; 23 | 24 | _interceptor.AfterSendAsync += InterceptResponseAsync; 25 | } 26 | 27 | #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize 28 | public void Dispose() 29 | #pragma warning restore CA1816 // Dispose methods should call SuppressFinalize 30 | { 31 | _interceptor.AfterSendAsync -= InterceptResponseAsync; 32 | } 33 | 34 | private async Task InterceptResponseAsync(object sender, HttpClientInterceptorEventArgs e) 35 | { 36 | if (e.Response.IsSuccessStatusCode) return; 37 | 38 | var capturedContent = await e.GetCapturedContentAsync(); 39 | var response = await capturedContent.ReadFromJsonAsync(); 40 | 41 | switch (e.Response.StatusCode) 42 | { 43 | case HttpStatusCode.NotFound: 44 | _nav.NavigateTo("/404"); 45 | break; 46 | 47 | case HttpStatusCode.Forbidden: 48 | case HttpStatusCode.BadRequest: 49 | _snackbar.Add(response.Message, Severity.Error); 50 | break; 51 | 52 | default: 53 | _nav.NavigateTo("/server-error"); 54 | break; 55 | } 56 | 57 | e.Response.EnsureSuccessStatusCode(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BugLab.Blazor/Interceptors/RefreshTokenInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using BugLab.Blazor.Helpers; 3 | using BugLab.Shared.Requests.Auth; 4 | using BugLab.Shared.Responses; 5 | using System; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Net.Http.Json; 9 | using System.Threading.Tasks; 10 | using Toolbelt.Blazor; 11 | 12 | namespace BugLab.Blazor.Interceptors 13 | { 14 | public class RefreshTokenInterceptor : IDisposable 15 | { 16 | private IHttpClientInterceptor _interceptor; 17 | private readonly ILocalStorageService _localStorage; 18 | private readonly HttpClient _client; 19 | 20 | public RefreshTokenInterceptor(IHttpClientInterceptor Interceptor, ILocalStorageService localStorage, HttpClient client) 21 | { 22 | _interceptor = Interceptor; 23 | _localStorage = localStorage; 24 | _client = client; 25 | _interceptor.BeforeSendAsync += InterceptRequestAsync; 26 | } 27 | 28 | private async Task InterceptRequestAsync(object sender, HttpClientInterceptorEventArgs e) 29 | { 30 | var requestPath = e.Request.RequestUri.AbsolutePath; 31 | if (requestPath.Contains(Endpoints.Auth) || requestPath.Contains("token")) return; 32 | 33 | var user = await _localStorage.GetItemAsync("user"); 34 | var claims = TokenHelper.ParseClaims(user.Token); 35 | var expirationDate = claims.GetExpirationDate(); 36 | if (expirationDate > DateTime.UtcNow.AddMinutes(10)) return; 37 | 38 | var request = new RefreshTokenRequest { AccessToken = user.Token, RefreshToken = user.RefreshToken }; 39 | var result = await _client.PostAsJsonAsync(Endpoints.Token, request); 40 | result.EnsureSuccessStatusCode(); 41 | 42 | user.Token = await result.Content.ReadAsStringAsync(); 43 | await _localStorage.SetItemAsync("user", user); 44 | _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", user.Token); 45 | } 46 | 47 | public void Dispose() 48 | { 49 | _interceptor.BeforeSendAsync -= InterceptRequestAsync; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BugLab.Blazor/Pages/EmailConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/confirm-email/{userId}/token/{token}" 2 | @inject HttpClient Client 3 | 4 | @_message 5 | Home 6 | 7 | @code{ 8 | private string _message = "Confirming...."; 9 | 10 | [Parameter] public string UserId { get; set; } 11 | [Parameter] public string Token { get; set; } 12 | 13 | protected override async Task OnParametersSetAsync() 14 | { 15 | if (string.IsNullOrWhiteSpace(Token)) 16 | { 17 | _message = "The link was broken. Please try again"; 18 | return; 19 | } 20 | 21 | var uri = QueryBuilder.Use($"{Endpoints.Auth}/{UserId}/confirm-email") 22 | .WithParam(nameof(Token), Token).Build(); 23 | 24 | await Client.PostAsync(uri, null); 25 | _message = "Good job! It's time to resolve some bugs"; 26 | } 27 | } -------------------------------------------------------------------------------- /BugLab.Blazor/Pages/Errors/NotFound.razor: -------------------------------------------------------------------------------- 1 | @page "/not-found" 2 | 3 | The requested resource was not found. 4 | 5 | Home -------------------------------------------------------------------------------- /BugLab.Blazor/Pages/Errors/ServerError.razor: -------------------------------------------------------------------------------- 1 | @page "/server-error" 2 | Something went wrong, please contact an Administrator -------------------------------------------------------------------------------- /BugLab.Blazor/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @page "/home" 3 | @page "/index" 4 | @page "/dashboard" 5 | 6 | 7 | 8 | Welcome @context.User.FindFirst(ClaimTypes.Email).Value 9 | 10 | Projects 11 | My Bugs 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BugLab.Blazor/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using BugLab.Blazor.Helpers; 3 | using BugLab.Blazor.Interceptors; 4 | using Microsoft.AspNetCore.Components.Authorization; 5 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using MudBlazor.Services; 8 | using System; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | using Toolbelt.Blazor.Extensions.DependencyInjection; 12 | 13 | namespace BugLab.Blazor 14 | { 15 | public class Program 16 | { 17 | public static async Task Main(string[] args) 18 | { 19 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 20 | builder.RootComponents.Add("#app"); 21 | builder.Services.AddHttpClientInterceptor(); 22 | 23 | builder.Services.AddScoped(sp => new HttpClient 24 | { 25 | BaseAddress = new Uri("https://localhost:5001/") 26 | }.EnableIntercept(sp)); 27 | 28 | builder.Services.AddScoped(); 29 | builder.Services.AddScoped(); 30 | 31 | builder.Services.AddMudServices().AddMudBlazorSnackbar(cfg => 32 | { 33 | cfg.ClearAfterNavigation = false; 34 | cfg.ShowCloseIcon = true; 35 | cfg.PreventDuplicates = false; 36 | }); 37 | 38 | builder.Services.AddBlazoredLocalStorage(); 39 | builder.Services.AddScoped(); 40 | builder.Services.AddScoped(p => p.GetRequiredService()); 41 | builder.Services.AddAuthorizationCore(); 42 | 43 | await builder.Build().RunAsync(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BugLab.Blazor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58296", 7 | "sslPort": 44387 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "BugLab.Blazor": { 20 | "commandName": "Project", 21 | "dotnetRunMessages": "true", 22 | "launchBrowser": true, 23 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 24 | "applicationUrl": "https://localhost:4201;http://localhost:4200", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Blazor/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using BugLab.Blazor.Interceptors 2 | 3 | @inherits LayoutComponentBase 4 | 5 | @inject AuthState AuthState 6 | @inject RefreshTokenInterceptor RefreshTokenInterceptor 7 | @inject ExceptionInterceptor ExceptionInterceptor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Logged in as @context.User.FindFirst(ClaimTypes.Email).Value 20 | 21 | 22 | 23 | 24 | 25 | 26 | Bug Lab 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | @Body 35 | 36 | 37 | 38 | 39 | @code { 40 | bool _drawerOpen = true; 41 | 42 | void DrawerToggle() 43 | { 44 | _drawerOpen = !_drawerOpen; 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /BugLab.Blazor/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/Shared/MainLayout.razor.css -------------------------------------------------------------------------------- /BugLab.Blazor/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  2 | Home 3 | Projects 4 | Bugs 5 | -------------------------------------------------------------------------------- /BugLab.Blazor/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Microsoft.AspNetCore.Authorization 10 | @using Microsoft.AspNetCore.Components.Authorization 11 | @using System.Security.Claims 12 | @using MudBlazor 13 | @using Blazored.FluentValidation 14 | @using Mapster 15 | @using System.Text.Json 16 | @using System.Text 17 | 18 | @using BugLab.Blazor 19 | @using BugLab.Blazor.Shared 20 | @using BugLab.Blazor.Helpers 21 | @using BugLab.Blazor.Components 22 | @using BugLab.Blazor.Components.Bugs 23 | @using BugLab.Blazor.Components.Auth 24 | @using BugLab.Blazor.Components.Sprints 25 | @using BugLab.Blazor.Components.Projects 26 | @using BugLab.Blazor.Dialogs 27 | @using BugLab.Blazor.Components.BugTypes 28 | @using BugLab.Blazor.Components.Users 29 | 30 | @using BugLab.Shared.Responses 31 | @using BugLab.Shared.Requests.Bugs 32 | @using BugLab.Shared.Requests.Comments 33 | @using BugLab.Shared.Requests.Projects 34 | @using BugLab.Shared.Requests.Auth 35 | @using BugLab.Shared.Requests.Sprints 36 | 37 | @using BugLab.Shared.QueryParams 38 | @using BugLab.Shared.Headers 39 | @using BugLab.Shared.Helpers.HttpClientHelpers 40 | @using BugLab.Shared.Validators 41 | @using BugLab.Shared.Enums 42 | @using BugLab.Shared.Requests.BugTypes 43 | -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisK00/BugLab/2e462e43d5fd939db75e3b20fb4a6f77d725454e/BugLab.Blazor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /BugLab.Blazor/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BugLab.Blazor 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 |
Loading...
22 | 23 |
24 | An unhandled error has occurred. 25 | Reload 26 | 🗙 27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /BugLab.Business/BackgroundServices/RemoveDeletedBugsService.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.BackgroundServices 12 | { 13 | public class RemoveDeletedBugsService : BackgroundService 14 | { 15 | private readonly IServiceScopeFactory _serviceScopeFactory; 16 | private readonly ILogger _logger; 17 | 18 | public RemoveDeletedBugsService(IServiceScopeFactory serviceScopeFactory, ILogger logger) 19 | { 20 | _serviceScopeFactory = serviceScopeFactory; 21 | _logger = logger; 22 | } 23 | 24 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 25 | { 26 | while (!stoppingToken.IsCancellationRequested) 27 | { 28 | _logger.LogWarning("Removing deleted bugs"); 29 | using var scope = _serviceScopeFactory.CreateScope(); 30 | 31 | try 32 | { 33 | var context = scope.ServiceProvider.GetRequiredService(); 34 | var deletedBugsCount = await RemoveDeletedBugs(context, stoppingToken); 35 | 36 | _logger.LogInformation("Finished removing {Count} deleted bugs", deletedBugsCount); 37 | await Task.Delay(TimeSpan.FromDays(14), stoppingToken); 38 | } 39 | catch (OperationCanceledException) 40 | { 41 | return; 42 | } 43 | } 44 | } 45 | 46 | private async Task RemoveDeletedBugs(AppDbContext context, CancellationToken stoppingToken) 47 | { 48 | var deletedBugs = await context.Bugs.IgnoreQueryFilters() 49 | .Where(x => x.Deleted.HasValue && x.Deleted.Value < DateTime.UtcNow.AddDays(-30)) 50 | .ToListAsync(stoppingToken); 51 | 52 | context.Bugs.RemoveRange(deletedBugs); 53 | await context.SaveChangesAsync(stoppingToken); 54 | 55 | return deletedBugs.Count; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BugLab.Business/BugLab.Business.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Auth/ConfirmEmailHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Business.Helpers; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.WebUtilities; 6 | using System; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.CommandHandlers.Auth 12 | { 13 | public class ConfirmEmailHandler : IRequestHandler 14 | { 15 | private readonly UserManager _userManager; 16 | 17 | public ConfirmEmailHandler(UserManager userManager) 18 | { 19 | _userManager = userManager; 20 | } 21 | 22 | public async Task Handle(ConfirmEmailCommand request, CancellationToken cancellationToken) 23 | { 24 | var user = await _userManager.FindByIdAsync(request.UserId); 25 | Guard.NotFound(user, nameof(user), request.UserId); 26 | 27 | var decodedToken = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(request.ConfirmationToken)); 28 | var result = await _userManager.ConfirmEmailAsync(user, decodedToken); 29 | if (result.Succeeded) return Unit.Value; 30 | 31 | var sb = new StringBuilder(); 32 | foreach (var item in result.Errors) 33 | { 34 | sb.Append(item.Description); 35 | } 36 | 37 | throw new InvalidOperationException(sb.ToString()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Auth/LoginHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Business.Interfaces; 3 | using BugLab.Data; 4 | using BugLab.Data.Entities; 5 | using BugLab.Shared.Responses; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | using System; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.Business.CommandHandlers.Auth 13 | { 14 | public class LoginHandler : IRequestHandler 15 | { 16 | private readonly SignInManager _signInManager; 17 | private readonly ITokenService _tokenService; 18 | private readonly AppDbContext _context; 19 | private readonly UserManager _userManager; 20 | 21 | public LoginHandler(SignInManager signInManager, UserManager userManager, 22 | ITokenService tokenService, AppDbContext context) 23 | { 24 | _signInManager = signInManager; 25 | _tokenService = tokenService; 26 | _context = context; 27 | _userManager = userManager; 28 | } 29 | 30 | public async Task Handle(LoginCommand request, CancellationToken cancellationToken) 31 | { 32 | var user = await _userManager.FindByEmailAsync(request.Email); 33 | _ = user ?? throw new UnauthorizedAccessException("Failed to login"); 34 | 35 | var loginResult = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false); 36 | if (!loginResult.Succeeded) throw new UnauthorizedAccessException("Invalid username or password"); 37 | 38 | var userResponse = new LoginResponse 39 | { 40 | Email = user.Email, 41 | Id = user.Id, 42 | EmailConfirmed = user.EmailConfirmed 43 | }; 44 | 45 | userResponse.Token = await _tokenService.GetJwtTokenAsync(user); 46 | userResponse.RefreshToken = _tokenService.GetRefreshToken(); 47 | 48 | await _context.RefreshTokens.AddAsync(new RefreshToken 49 | { 50 | UserId = user.Id, 51 | Value = userResponse.RefreshToken 52 | }, cancellationToken); 53 | 54 | await _context.SaveChangesAsync(cancellationToken); 55 | 56 | return userResponse; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Auth/RefreshTokenHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Business.Interfaces; 4 | using BugLab.Data; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.IdentityModel.Tokens; 8 | using System; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.Business.CommandHandlers.Auth 13 | { 14 | public class RefreshTokenHandler : IRequestHandler 15 | { 16 | private readonly AppDbContext _context; 17 | private readonly ITokenService _tokenService; 18 | 19 | public RefreshTokenHandler(AppDbContext context, ITokenService tokenService) 20 | { 21 | _context = context; 22 | _tokenService = tokenService; 23 | } 24 | 25 | public async Task Handle(RefreshTokenCommand request, CancellationToken cancellationToken) 26 | { 27 | var refreshToken = await _context.RefreshTokens.Include(x => x.User) 28 | .FirstOrDefaultAsync(x => x.Value == request.RefreshToken, cancellationToken); 29 | 30 | Guard.NotFound(refreshToken, "refresh token"); 31 | 32 | if (refreshToken.ExpirationDate <= DateTime.UtcNow) throw new SecurityTokenException("Token has expired"); 33 | _tokenService.ValidateToken(request.AccessToken); 34 | 35 | return await _tokenService.GetJwtTokenAsync(refreshToken.User); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Auth/RegisterHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Business.Interfaces; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Identity; 5 | using System; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.Auth 11 | { 12 | public class RegisterHandler : IRequestHandler 13 | { 14 | private readonly UserManager _userManager; 15 | private readonly ITokenService _tokenService; 16 | 17 | public RegisterHandler(UserManager userManager, ITokenService tokenService) 18 | { 19 | _userManager = userManager; 20 | _tokenService = tokenService; 21 | } 22 | 23 | public async Task Handle(RegisterCommand request, CancellationToken cancellationToken) 24 | { 25 | var user = new IdentityUser { Email = request.Email, UserName = request.Email }; 26 | var registerResult = await _userManager.CreateAsync(user, request.Password); 27 | 28 | if (!registerResult.Succeeded) 29 | { 30 | var sb = new StringBuilder(); 31 | 32 | foreach (var error in registerResult.Errors) 33 | { 34 | sb.Append(error.Description); 35 | } 36 | 37 | throw new InvalidOperationException(sb.ToString()); 38 | } 39 | 40 | await _tokenService.SendEmailConfirmationAsync(user); 41 | 42 | return Unit.Value; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Auth/ResendEmailConfirmationHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Auth; 2 | using BugLab.Business.Interfaces; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Identity; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.Auth 10 | { 11 | public class ResendEmailConfirmationHandler : IRequestHandler 12 | { 13 | private readonly ITokenService _tokenService; 14 | private readonly UserManager _userManager; 15 | 16 | public ResendEmailConfirmationHandler(ITokenService tokenService, UserManager userManager) 17 | { 18 | _tokenService = tokenService; 19 | _userManager = userManager; 20 | } 21 | public async Task Handle(ResendEmailConfirmationCommand request, CancellationToken cancellationToken) 22 | { 23 | var user = await _userManager.FindByIdAsync(request.UserId); 24 | if (user.EmailConfirmed) throw new InvalidOperationException("You have already confirmed your email"); 25 | 26 | await _tokenService.SendEmailConfirmationAsync(user); 27 | 28 | return Unit.Value; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/BugTypes/AddBugTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.BugTypes; 2 | using BugLab.Data; 3 | using BugLab.Data.Entities; 4 | using Mapster; 5 | using MediatR; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.BugTypes 10 | { 11 | public class AddBugTypeHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public AddBugTypeHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public async Task Handle(AddBugTypeCommand request, CancellationToken cancellationToken) 21 | { 22 | var newBugType = request.Adapt(); 23 | await _context.BugTypes.AddAsync(newBugType, cancellationToken); 24 | await _context.SaveChangesAsync(cancellationToken); 25 | 26 | return newBugType.Id; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/BugTypes/DeleteBugTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.BugTypes; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.BugTypes 11 | { 12 | public class DeleteBugTypeHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public DeleteBugTypeHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(DeleteBugTypeCommand request, CancellationToken cancellationToken) 22 | { 23 | var bugType = await _context.BugTypes.FirstOrDefaultAsync(bt => bt.Id == request.Id, cancellationToken); 24 | Guard.NotFound(bugType, "bug type", request.Id); 25 | 26 | var hasRelatedBugs = await _context.Bugs.AnyAsync(b => b.BugTypeId == request.Id, cancellationToken); 27 | if (hasRelatedBugs) throw new InvalidOperationException("You can't delete a bug type that is being used by other bugs"); 28 | 29 | _context.BugTypes.Remove(bugType); 30 | await _context.SaveChangesAsync(cancellationToken); 31 | 32 | return Unit.Value; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/BugTypes/UpdateBugTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.BugTypes; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.BugTypes 10 | { 11 | public class UpdateBugTypeHandler : IRequestHandler 12 | { 13 | 14 | private readonly AppDbContext _context; 15 | 16 | public UpdateBugTypeHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(UpdateBugTypeCommand request, CancellationToken cancellationToken) 22 | { 23 | var bugType = await _context.BugTypes.FirstOrDefaultAsync(bt => bt.Id == request.Id, cancellationToken); 24 | Guard.NotFound(bugType, "bug type", request.Id); 25 | 26 | bugType.Title = request.Title; 27 | bugType.Color = request.Color; 28 | await _context.SaveChangesAsync(cancellationToken); 29 | 30 | return Unit.Value; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Bugs/AddBugHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Bugs; 2 | using BugLab.Data; 3 | using BugLab.Data.Entities; 4 | using MediatR; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace BugLab.Business.CommandHandlers.Bugs 9 | { 10 | public class AddBugHandler : IRequestHandler 11 | { 12 | private readonly AppDbContext _context; 13 | 14 | public AddBugHandler(AppDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public async Task Handle(AddBugCommand request, CancellationToken cancellationToken) 20 | { 21 | var newBug = new Bug 22 | { 23 | Title = request.Title, 24 | Description = request.Description, 25 | Priority = request.Priority, 26 | Status = request.Status, 27 | ProjectId = request.ProjectId, 28 | BugTypeId = request.TypeId, 29 | AssignedToId = string.IsNullOrWhiteSpace(request.AssignedToId) ? null : request.AssignedToId, 30 | }; 31 | 32 | await _context.Bugs.AddAsync(newBug, cancellationToken); 33 | await _context.SaveChangesAsync(cancellationToken); 34 | 35 | return newBug.Id; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Bugs/DeleteBugHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Bugs; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.Bugs 11 | { 12 | public class DeleteBugHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public DeleteBugHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(DeleteBugCommand request, CancellationToken cancellationToken) 22 | { 23 | var bug = await _context.Bugs.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken); 24 | Guard.NotFound(bug, nameof(bug), request.Id); 25 | 26 | bug.Deleted = DateTime.UtcNow; 27 | await _context.SaveChangesAsync(cancellationToken); 28 | 29 | return Unit.Value; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Bugs/UpdateBugHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Bugs; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.Bugs 11 | { 12 | public class UpdateBugHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public UpdateBugHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(UpdateBugCommand request, CancellationToken cancellationToken) 22 | { 23 | var bug = await _context.Bugs.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken); 24 | Guard.NotFound(bug, nameof(bug), request.Id); 25 | 26 | if (request.PartialUpdate) request.Adapt(bug, new TypeAdapterConfig().Default.IgnoreNullValues(true).Config); 27 | else request.Adapt(bug); 28 | 29 | await _context.SaveChangesAsync(cancellationToken); 30 | 31 | return Unit.Value; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Comments/AddCommentHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Comments; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Data.Entities; 5 | using Mapster; 6 | using MediatR; 7 | using Microsoft.EntityFrameworkCore; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.CommandHandlers.Comments 12 | { 13 | public class AddCommentHandler : IRequestHandler 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public AddCommentHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task Handle(AddCommentCommand request, CancellationToken cancellationToken) 23 | { 24 | var bug = await _context.Bugs.Include(x => x.Comments).FirstOrDefaultAsync(x => x.Id == request.BugId, cancellationToken); 25 | Guard.NotFound(bug, nameof(bug), request.BugId); 26 | 27 | var comment = request.Adapt(); 28 | bug.Comments.Add(comment); 29 | await _context.SaveChangesAsync(cancellationToken); 30 | 31 | return Unit.Value; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Comments/DeleteCommentHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Comments; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.Comments 11 | { 12 | public class DeleteCommentHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public DeleteCommentHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(DeleteCommentCommand request, CancellationToken cancellationToken) 22 | { 23 | var bug = await _context.Bugs.Include(x => x.Comments).FirstOrDefaultAsync(x => x.Id == request.BugId, cancellationToken); 24 | Guard.NotFound(bug, nameof(bug), request.BugId); 25 | 26 | var commentToRemove = bug.Comments.FirstOrDefault(x => x.Id == request.CommentId); 27 | Guard.NotFound(commentToRemove, "comment", request.CommentId); 28 | bug.Comments.Remove(commentToRemove); 29 | await _context.SaveChangesAsync(cancellationToken); 30 | 31 | return Unit.Value; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Comments/UpdateCommentHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Comments; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.CommandHandlers.Comments 11 | { 12 | public class UpdateCommentHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public UpdateCommentHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(UpdateCommentCommand request, CancellationToken cancellationToken) 22 | { 23 | var bug = await _context.Bugs.Include(x => x.Comments).FirstOrDefaultAsync(x => x.Id == request.BugId, cancellationToken); 24 | Guard.NotFound(bug, nameof(bug), request.BugId); 25 | 26 | var comment = bug.Comments.FirstOrDefault(c => c.Id == request.Id); 27 | comment.Text = request.Text; 28 | await _context.SaveChangesAsync(cancellationToken); 29 | 30 | return Unit.Value; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Projects/AddProjectHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Data; 3 | using BugLab.Data.Entities; 4 | using Mapster; 5 | using MediatR; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.Projects 10 | { 11 | public class AddProjectHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public AddProjectHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public async Task Handle(AddProjectCommand request, CancellationToken cancellationToken) 21 | { 22 | var projectToAdd = request.Adapt(); 23 | 24 | using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken); 25 | 26 | await _context.Projects.AddAsync(projectToAdd, cancellationToken); 27 | await _context.SaveChangesAsync(cancellationToken); 28 | 29 | await _context.ProjectUsers.AddAsync(new ProjectUser { UserId = request.UserId, ProjectId = projectToAdd.Id }, cancellationToken); 30 | 31 | await AddDefaultBugTypes(projectToAdd.Id); 32 | 33 | await _context.SaveChangesAsync(cancellationToken); 34 | await transaction.CommitAsync(cancellationToken); 35 | 36 | return projectToAdd.Id; 37 | } 38 | 39 | private async Task AddDefaultBugTypes(int projectId) 40 | { 41 | await _context.BugTypes.AddRangeAsync( 42 | new BugType { ProjectId = projectId, Title = "refactor", Color = "#977FE4" }, 43 | new BugType { ProjectId = projectId, Title = "bug", Color = "#b14639ff" }, 44 | new BugType { ProjectId = projectId, Title = "feature", Color = "#35ceceff" } 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Projects/AddProjectUsersHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Data.Entities; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.Business.CommandHandlers.Projects 14 | { 15 | public class AddProjectUsersHandler : IRequestHandler 16 | { 17 | private readonly AppDbContext _context; 18 | 19 | public AddProjectUsersHandler(AppDbContext context) 20 | { 21 | _context = context; 22 | } 23 | 24 | public async Task Handle(AddProjectUsersCommand request, CancellationToken cancellationToken) 25 | { 26 | var projectUsers = await _context.ProjectUsers.Where(x => x.ProjectId == request.ProjectId).ToListAsync(cancellationToken); 27 | 28 | Guard.NotFound(projectUsers, "project", request.ProjectId); 29 | 30 | var users = await _context.Users.Where(u => request.UserIds.Contains(u.Id)).ToListAsync(cancellationToken); 31 | Guard.NotFound(users, nameof(users)); 32 | 33 | IReadOnlyCollection projectUsersToAdd = users.Where(u => !projectUsers.Any(pu => pu.UserId == u.Id)) 34 | .Select(u => new ProjectUser { UserId = u.Id, ProjectId = request.ProjectId }) 35 | .ToList(); 36 | 37 | await _context.ProjectUsers.AddRangeAsync(projectUsersToAdd, cancellationToken); 38 | await _context.SaveChangesAsync(cancellationToken); 39 | 40 | if (projectUsersToAdd.Count != users.Count) 41 | { 42 | throw new InvalidOperationException("Some users were not added because they could not be found" + 43 | " or they are already members of this project"); 44 | } 45 | 46 | return Unit.Value; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Projects/DeleteProjectHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Shared.Enums; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.Business.CommandHandlers.Projects 13 | { 14 | public class DeleteProjectHandler : IRequestHandler 15 | { 16 | private readonly AppDbContext _context; 17 | 18 | public DeleteProjectHandler(AppDbContext context) 19 | { 20 | _context = context; 21 | } 22 | 23 | public async Task Handle(DeleteProjectCommand request, CancellationToken cancellationToken) 24 | { 25 | var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken); 26 | Guard.NotFound(project, nameof(project), request.Id); 27 | 28 | if (await _context.Bugs.AnyAsync(b => b.ProjectId == project.Id && b.Status != BugStatus.Resolved, cancellationToken)) 29 | { 30 | throw new InvalidOperationException("remove or resolve existing bugs before deletion"); 31 | } 32 | 33 | _context.Projects.Remove(project); 34 | await _context.SaveChangesAsync(cancellationToken); 35 | 36 | return Unit.Value; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Projects/DeleteProjectUserHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.Projects 10 | { 11 | public class DeleteProjectUserHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public DeleteProjectUserHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public async Task Handle(DeleteProjectUserCommand request, CancellationToken cancellationToken) 21 | { 22 | var projectUser = await _context.ProjectUsers.FirstOrDefaultAsync(pu => pu.ProjectId == request.ProjectId && pu.UserId == request.UserId, cancellationToken); 23 | Guard.NotFound(projectUser, "user", request.UserId); 24 | 25 | _context.ProjectUsers.Remove(projectUser); 26 | await _context.SaveChangesAsync(cancellationToken); 27 | return Unit.Value; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Projects/UpdateProjectHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Projects; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.CommandHandlers.Projects 10 | { 11 | public class UpdateProjectHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public UpdateProjectHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public async Task Handle(UpdateProjectCommand request, CancellationToken cancellationToken) 21 | { 22 | var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken); 23 | Guard.NotFound(project, nameof(project), project.Id); 24 | 25 | project.Title = request.Title; 26 | project.Description = request.Description; 27 | 28 | await _context.SaveChangesAsync(cancellationToken); 29 | 30 | return Unit.Value; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BugLab.Business/CommandHandlers/Sprints/AddSprintHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Commands.Sprints; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Data.Entities; 5 | using Mapster; 6 | using MediatR; 7 | using Microsoft.EntityFrameworkCore; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.CommandHandlers.Sprints 12 | { 13 | public class DeleteSprintHandler : IRequestHandler 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public DeleteSprintHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task Handle(DeleteSprintCommand request, CancellationToken cancellationToken) 23 | { 24 | var sprint = await _context.Sprints.Include(x => x.Bugs).FirstOrDefaultAsync(x => x.Id == request.Id); 25 | Guard.NotFound(sprint, nameof(sprint)); 26 | 27 | _context.Remove(sprint); 28 | _context.SaveChanges(); 29 | return Unit.Value; 30 | } 31 | } 32 | 33 | public class AddSprintHandler : IRequestHandler 34 | { 35 | private readonly AppDbContext _context; 36 | 37 | public AddSprintHandler(AppDbContext context) 38 | { 39 | _context = context; 40 | } 41 | 42 | public async Task Handle(AddSprintCommand request, CancellationToken cancellationToken) 43 | { 44 | var newSprint = request.Adapt(); 45 | await _context.AddAsync(newSprint, cancellationToken); 46 | await _context.SaveChangesAsync(cancellationToken); 47 | 48 | return newSprint.Id; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Auth/ConfirmEmailCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Auth 4 | { 5 | public class ConfirmEmailCommand : IRequest 6 | { 7 | public ConfirmEmailCommand(string userId, string confirmationToken) 8 | { 9 | UserId = userId; 10 | ConfirmationToken = confirmationToken; 11 | } 12 | 13 | public string UserId { get; } 14 | public string ConfirmationToken { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Auth/LoginCommand.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Commands.Auth 5 | { 6 | public class LoginCommand : IRequest 7 | { 8 | public LoginCommand(string email, string password) 9 | { 10 | Email = email; 11 | Password = password; 12 | } 13 | 14 | public string Email { get; } 15 | public string Password { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Auth/RefreshTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Auth 4 | { 5 | public class RefreshTokenCommand : IRequest 6 | { 7 | public RefreshTokenCommand(string refreshToken, string accessToken) 8 | { 9 | RefreshToken = refreshToken; 10 | AccessToken = accessToken; 11 | } 12 | 13 | public string RefreshToken { get; } 14 | public string AccessToken { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Auth/RegisterCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Auth 4 | { 5 | public class RegisterCommand : IRequest 6 | { 7 | public RegisterCommand(string email, string password) 8 | { 9 | Email = email; 10 | Password = password; 11 | } 12 | 13 | public string Email { get; } 14 | public string Password { get;} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Auth/ResendEmailConfirmationCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Auth 4 | { 5 | public class ResendEmailConfirmationCommand : IRequest 6 | { 7 | public ResendEmailConfirmationCommand(string userId) 8 | { 9 | UserId = userId; 10 | } 11 | 12 | public string UserId { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/BugTypes/AddBugTypeCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.BugTypes 4 | { 5 | public class AddBugTypeCommand : IRequest 6 | { 7 | public AddBugTypeCommand(int projectId, string color, string title) 8 | { 9 | ProjectId = projectId; 10 | Color = color; 11 | Title = title; 12 | } 13 | 14 | public int ProjectId { get; } 15 | public string Color { get; } 16 | public string Title { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/BugTypes/DeleteBugTypeCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.BugTypes 4 | { 5 | public class DeleteBugTypeCommand : IRequest 6 | { 7 | public DeleteBugTypeCommand(int id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public int Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/BugTypes/UpdateBugTypeCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.BugTypes 4 | { 5 | public class UpdateBugTypeCommand : IRequest 6 | { 7 | public UpdateBugTypeCommand(int id, string title, string color) 8 | { 9 | Id = id; 10 | Title = title; 11 | Color = color; 12 | } 13 | 14 | public int Id { get; } 15 | public string Title { get; } 16 | public string Color { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Bugs/AddBugCommand.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Commands.Bugs 5 | { 6 | public class AddBugCommand : IRequest 7 | { 8 | public AddBugCommand(string title, string description, BugPriority priority, BugStatus status, int typeId, 9 | int projectId, string assignedToId, int? sprintId) 10 | { 11 | Title = title; 12 | Description = description; 13 | Priority = priority; 14 | Status = status; 15 | TypeId = typeId; 16 | ProjectId = projectId; 17 | AssignedToId = assignedToId; 18 | SprintId = sprintId; 19 | } 20 | 21 | public string Title { get; } 22 | public string Description { get; } 23 | public BugPriority Priority { get; } 24 | public BugStatus Status { get; } 25 | public int TypeId { get; } 26 | public int ProjectId { get; } 27 | public string AssignedToId { get; } 28 | public int? SprintId { get; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Bugs/DeleteBugCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Bugs 4 | { 5 | public class DeleteBugCommand : IRequest 6 | { 7 | public DeleteBugCommand(int id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public int Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Bugs/UpdateBugCommand.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Commands.Bugs 5 | { 6 | public class UpdateBugCommand : IRequest 7 | { 8 | public UpdateBugCommand(int id, string title, string description, BugPriority priority, BugStatus status, 9 | int typeId, string assignedToId, int? sprintId, bool partialUpdate = false) 10 | { 11 | Id = id; 12 | Title = title; 13 | Description = description; 14 | Priority = priority; 15 | Status = status; 16 | TypeId = typeId; 17 | AssignedToId = assignedToId; 18 | SprintId = sprintId; 19 | PartialUpdate = partialUpdate; 20 | AssignedToId = string.IsNullOrWhiteSpace(assignedToId) ? null : assignedToId; 21 | } 22 | 23 | public int Id { get; } 24 | public string Title { get; } 25 | public string Description { get; } 26 | public BugPriority Priority { get; } 27 | public BugStatus Status { get; } 28 | public int TypeId { get; } 29 | public string AssignedToId { get; } 30 | public int? SprintId { get; set; } 31 | public bool PartialUpdate { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Comments/AddCommentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Comments 4 | { 5 | public class AddCommentCommand : IRequest 6 | { 7 | public AddCommentCommand(int bugId, string text) 8 | { 9 | BugId = bugId; 10 | Text = text; 11 | } 12 | 13 | public int BugId { get; } 14 | public string Text { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Comments/DeleteCommentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Comments 4 | { 5 | public class DeleteCommentCommand : IRequest 6 | { 7 | public DeleteCommentCommand(int commentId, int bugId) 8 | { 9 | CommentId = commentId; 10 | BugId = bugId; 11 | } 12 | 13 | public int CommentId { get; } 14 | public int BugId { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Comments/UpdateCommentCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Comments 4 | { 5 | public class UpdateCommentCommand : IRequest 6 | { 7 | public UpdateCommentCommand(int id, int bugId, string text) 8 | { 9 | Id = id; 10 | BugId = bugId; 11 | Text = text; 12 | } 13 | 14 | public int BugId { get; } 15 | public int Id { get; } 16 | public string Text { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /BugLab.Business/Commands/Projects/AddProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Projects 4 | { 5 | public class AddProjectCommand : IRequest 6 | { 7 | public AddProjectCommand(string userId, string title, string description) 8 | { 9 | UserId = userId; 10 | Title = title; 11 | Description = description; 12 | } 13 | 14 | public string UserId { get; } 15 | public string Title { get; } 16 | public string Description { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Projects/AddProjectUsersCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System.Collections.Generic; 3 | 4 | namespace BugLab.Business.Commands.Projects 5 | { 6 | public class AddProjectUsersCommand : IRequest 7 | { 8 | public AddProjectUsersCommand(int projectId, IEnumerable userIds) 9 | { 10 | ProjectId = projectId; 11 | UserIds = userIds; 12 | } 13 | 14 | public int ProjectId { get; } 15 | public IEnumerable UserIds { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Projects/DeleteProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Projects 4 | { 5 | public class DeleteProjectCommand : IRequest 6 | { 7 | public DeleteProjectCommand(int id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public int Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Projects/DeleteProjectUserCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Projects 4 | { 5 | public class DeleteProjectUserCommand : IRequest 6 | { 7 | public DeleteProjectUserCommand(int projectId, string userId) 8 | { 9 | ProjectId = projectId; 10 | UserId = userId; 11 | } 12 | 13 | public int ProjectId { get; } 14 | public string UserId { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Projects/UpdateProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Projects 4 | { 5 | public class UpdateProjectCommand : IRequest 6 | { 7 | public UpdateProjectCommand(int id, string title, string description) 8 | { 9 | Id = id; 10 | Title = title; 11 | Description = description; 12 | } 13 | 14 | public int Id { get; } 15 | public string Title { get; } 16 | public string Description { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Sprints/AddSprintCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | 4 | namespace BugLab.Business.Commands.Sprints 5 | { 6 | public class AddSprintCommand : IRequest 7 | { 8 | public AddSprintCommand(int projectId, string title, DateTime startDate, DateTime endDate) 9 | { 10 | ProjectId = projectId; 11 | Title = title; 12 | StartDate = startDate; 13 | EndDate = endDate; 14 | } 15 | 16 | public int ProjectId { get; } 17 | public string Title { get; } 18 | public DateTime StartDate { get; } 19 | public DateTime EndDate { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BugLab.Business/Commands/Sprints/DeleteSprintCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BugLab.Business.Commands.Sprints 4 | { 5 | public class DeleteSprintCommand : IRequest 6 | { 7 | public DeleteSprintCommand(int id) 8 | { 9 | Id = id; 10 | } 11 | 12 | public int Id { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Business/Extensions/IQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.Extensions 10 | { 11 | public static class IQueryableExtensions 12 | { 13 | /// 14 | /// Gets the total amount of items in the database and then applies pagination to the query 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// The query paginated and the count before pagination was applied 21 | public static async Task<(IQueryable Query, int TotalItems)> PaginateAsync(this IQueryable source, int pageNumber, int pageSize, 22 | CancellationToken cancellationToken) 23 | { 24 | var totalItems = await source.CountAsync(cancellationToken); 25 | 26 | var query = source.Skip(pageSize * (pageNumber - 1)) 27 | .Take(pageSize); 28 | 29 | return (query, totalItems); 30 | } 31 | 32 | public static IOrderedQueryable SortBy(this IQueryable source, 33 | Expression> sortOn, SortOrder sortOrder = SortOrder.Ascending) 34 | { 35 | return sortOrder == SortOrder.Ascending ? source.OrderBy(sortOn) : source.OrderByDescending(sortOn); 36 | } 37 | 38 | public static IOrderedQueryable ThenSortBy(this IOrderedQueryable source, 39 | Expression> sortOn, SortOrder sortOrder = SortOrder.Ascending) 40 | { 41 | return sortOrder == SortOrder.Ascending ? source.ThenBy(sortOn) : source.ThenByDescending(sortOn); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BugLab.Business/Extensions/ServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.BackgroundServices; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Business.Interfaces; 4 | using BugLab.Business.PipelineBehaviors; 5 | using BugLab.Business.Services; 6 | using Mapster; 7 | using MediatR; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace BugLab.Business.Extensions 11 | { 12 | public static class ServicesExtensions 13 | { 14 | public static void AddBusinessServices(this IServiceCollection services) 15 | { 16 | services.AddMediatR(typeof(ServicesExtensions).Assembly); 17 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); 18 | services.AddMemoryCache(); 19 | 20 | TypeAdapterConfig.GlobalSettings.Scan(typeof(Mappings).Assembly); 21 | 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | 26 | services.AddHostedService(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BugLab.Business/Helpers/Guard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace BugLab.Business.Helpers 6 | { 7 | public static class Guard 8 | { 9 | public static void NotFound(TInput input, string inputName, TId id) 10 | { 11 | _ = input ?? throw new KeyNotFoundException($"The requested {inputName} with an id of {id} was not found"); 12 | } 13 | 14 | public static void NotFound(IEnumerable inputs, string inputsName = null, string message = null) 15 | { 16 | if (string.IsNullOrWhiteSpace(message)) message = $"The requested {inputsName ?? "items"} were not found"; 17 | if (inputs == null || !inputs.Any()) throw new KeyNotFoundException(message); 18 | } 19 | 20 | public static void NotFound(TInput input, string inputTitle) 21 | { 22 | _ = input ?? throw new KeyNotFoundException($"{inputTitle} was not found"); 23 | } 24 | 25 | public static void NotFound(bool input, string inputName, T id) 26 | { 27 | if (!input) throw new KeyNotFoundException($"The requested {inputName} with an id of {id} was not found"); 28 | } 29 | 30 | public static void NullOrWhitespace(string input, string inputName, string message = null) 31 | { 32 | Null(input, inputName, message); 33 | 34 | if (string.IsNullOrWhiteSpace(input)) throw new ArgumentException(message ?? $"{inputName} was empty"); 35 | } 36 | 37 | public static T Null(T input, string parameterName, string message = null) 38 | { 39 | if (input is not null) return input; 40 | 41 | if (string.IsNullOrWhiteSpace(message)) throw new ArgumentNullException(parameterName); 42 | 43 | throw new ArgumentNullException(message); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BugLab.Business/Helpers/Mappings.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using BugLab.Shared.Responses; 3 | using Mapster; 4 | 5 | namespace BugLab.Business.Helpers 6 | { 7 | public class Mappings : IRegister 8 | { 9 | public void Register(TypeAdapterConfig config) 10 | { 11 | config.NewConfig() 12 | .Map(dest => dest.ProjectTitle, src => src.Project.Title); 13 | 14 | config.Default.MapToConstructor(true); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Business/Helpers/PagedList.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Extensions; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.Helpers 10 | { 11 | public class PagedList : List 12 | { 13 | public PagedList(IEnumerable items, int pageNumber, int pageSize, int totalItems) 14 | { 15 | PageNumber = pageNumber; 16 | PageSize = pageSize; 17 | TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize); 18 | TotalItems = totalItems; 19 | AddRange(items); 20 | } 21 | 22 | public int PageNumber { get; init; } 23 | public int PageSize { get; init; } 24 | public int TotalItems { get; init; } 25 | public int TotalPages { get; init; } 26 | 27 | /// 28 | /// Applies pagination to the query 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A paginated list 35 | public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize, CancellationToken cancellationToken = default) 36 | { 37 | int totalItems; 38 | (source, totalItems) = await source.PaginateAsync(pageNumber, pageSize, cancellationToken); 39 | var items = await source.ToListAsync(cancellationToken); 40 | 41 | return new PagedList(items, pageNumber, pageSize, totalItems); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /BugLab.Business/Interfaces/IAuthService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BugLab.Business.Interfaces 4 | { 5 | public interface IAuthService 6 | { 7 | Task HasAccessToProject(string userId, int projectId); 8 | Task HasAccessToBug(string userId, int id); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BugLab.Business/Interfaces/ICacheable.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Business.Interfaces 2 | { 3 | public interface ICacheable 4 | { 5 | public string Key { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BugLab.Business/Interfaces/IEmailService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BugLab.Business.Interfaces 4 | { 5 | public interface IEmailService 6 | { 7 | Task SendAsync(string subject, string body, string to); 8 | Task SendEmailConfirmationAsync(string confirmationToken, string userId, string to); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BugLab.Business/Interfaces/ITokenService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System.Threading.Tasks; 3 | 4 | namespace BugLab.Business.Interfaces 5 | { 6 | public interface ITokenService 7 | { 8 | Task GetJwtTokenAsync(IdentityUser user); 9 | string GetRefreshToken(); 10 | Task SendEmailConfirmationAsync(IdentityUser user); 11 | void ValidateToken(string accessToken); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BugLab.Business/Options/ClientOptions.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Business.Options 2 | { 3 | public class ClientOptions 4 | { 5 | public string Uri { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BugLab.Business/Options/EmailOptions.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Business.Options 2 | { 3 | public class EmailOptions 4 | { 5 | public string Name { get; set; } 6 | public string From { get; set; } 7 | public string Password { get; set; } 8 | public int Port { get; set; } 9 | public string Host { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BugLab.Business/Options/JwtOptions.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Business.Options 2 | { 3 | public class JwtOptions 4 | { 5 | public string TokenKey { get; init; } 6 | public string ValidIssuer { get; init; } 7 | public string ValidAudience { get; init; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Business/PipelineBehaviors/CachingBehavior.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Interfaces; 2 | using MediatR; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.PipelineBehaviors 10 | { 11 | public class CachingBehavior : IPipelineBehavior where TRequest : ICacheable 12 | { 13 | private readonly IMemoryCache _cache; 14 | private readonly ILogger> _logger; 15 | 16 | public CachingBehavior(IMemoryCache cache, ILogger> logger) 17 | { 18 | _cache = cache; 19 | _logger = logger; 20 | } 21 | 22 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 23 | { 24 | var requestName = request.GetType().Name; 25 | TResponse response; 26 | 27 | if (_cache.TryGetValue(request.Key, out response)) 28 | { 29 | _logger.LogInformation("Returning cached value for {requestName}", requestName); 30 | return response; 31 | } 32 | 33 | response = await next(); 34 | if (response == null) return response; 35 | 36 | _logger.LogInformation("Caching {requestName} response with Cache Key: {Key}", requestName, request.Key); 37 | var cacheOptions = new MemoryCacheEntryOptions() 38 | .SetSlidingExpiration(TimeSpan.FromSeconds(3)) 39 | .SetAbsoluteExpiration(TimeSpan.FromSeconds(10)); 40 | 41 | _cache.Set(request.Key, response, cacheOptions); 42 | return response; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/BugTypes/GetBugTypeQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Queries.BugTypes 5 | { 6 | public class GetBugTypeQuery : IRequest 7 | { 8 | public GetBugTypeQuery(int id) 9 | { 10 | Id = id; 11 | } 12 | 13 | public int Id { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/BugTypes/GetBugTypesQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | using System.Collections.Generic; 4 | 5 | namespace BugLab.Business.Queries.BugTypes 6 | { 7 | public class GetBugTypesQuery : IRequest> 8 | { 9 | public GetBugTypesQuery(int projectId) 10 | { 11 | ProjectId = projectId; 12 | } 13 | 14 | public int ProjectId { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Bugs/GetBugQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Queries.Bugs 5 | { 6 | public class GetBugQuery : IRequest 7 | { 8 | public GetBugQuery(int id) 9 | { 10 | Id = id; 11 | } 12 | 13 | public int Id { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Bugs/GetBugsQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Helpers; 2 | using BugLab.Shared.QueryParams; 3 | using BugLab.Shared.Responses; 4 | using MediatR; 5 | 6 | namespace BugLab.Business.Queries.Bugs 7 | { 8 | public class GetBugsQuery : BugParams, IRequest> 9 | { 10 | public GetBugsQuery(string userId) 11 | { 12 | UserId = userId; 13 | } 14 | 15 | public string UserId { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Projects/GetProjectQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Queries.Projects 5 | { 6 | public class GetProjectQuery : IRequest 7 | { 8 | public GetProjectQuery(int id) 9 | { 10 | Id = id; 11 | } 12 | 13 | public int Id { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Projects/GetProjectUsersQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Interfaces; 2 | using BugLab.Shared.Responses; 3 | using MediatR; 4 | using System.Collections.Generic; 5 | 6 | namespace BugLab.Business.Queries.Projects 7 | { 8 | public class GetProjectUsersQuery : IRequest>, ICacheable 9 | { 10 | public GetProjectUsersQuery(int projectId) 11 | { 12 | ProjectId = projectId; 13 | } 14 | 15 | public int ProjectId { get; } 16 | public string Key => $"ProjectUsers-{ProjectId}"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Projects/GetProjectsQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Helpers; 2 | using BugLab.Shared.QueryParams; 3 | using BugLab.Shared.Responses; 4 | using MediatR; 5 | 6 | namespace BugLab.Business.Queries.Projects 7 | { 8 | public class GetProjectsQuery : PaginationParams, IRequest> 9 | { 10 | public GetProjectsQuery(string userId) 11 | { 12 | UserId = userId; 13 | } 14 | 15 | public string UserId { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Sprints/GetSprintQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Queries.Sprints 5 | { 6 | public class GetSprintQuery : IRequest 7 | { 8 | public GetSprintQuery(int id) 9 | { 10 | Id = id; 11 | } 12 | 13 | public int Id { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Sprints/GetSprintsQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | using System.Collections.Generic; 4 | 5 | namespace BugLab.Business.Queries.Sprints 6 | { 7 | public class GetSprintsQuery : IRequest> 8 | { 9 | public GetSprintsQuery(int projectId) 10 | { 11 | ProjectId = projectId; 12 | } 13 | 14 | public int ProjectId { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Users/GetDashboardQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Responses; 2 | using MediatR; 3 | 4 | namespace BugLab.Business.Queries.Users 5 | { 6 | public class GetDashboardQuery : IRequest 7 | { 8 | public GetDashboardQuery(string userId) 9 | { 10 | UserId = userId; 11 | } 12 | 13 | public string UserId { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Business/Queries/Users/GetUsersQuery.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Helpers; 2 | using BugLab.Shared.QueryParams; 3 | using BugLab.Shared.Responses; 4 | using MediatR; 5 | 6 | namespace BugLab.Business.Queries.Users 7 | { 8 | public class GetUsersQuery : UserParams, IRequest> 9 | { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/BugTypes/GetBugTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Shared.Responses; 3 | using Mapster; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.Queries.BugTypes 10 | { 11 | public class GetBugTypeHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public GetBugTypeHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public async Task Handle(GetBugTypeQuery request, CancellationToken cancellationToken) 21 | { 22 | return await _context.BugTypes.AsNoTracking().ProjectToType() 23 | .FirstOrDefaultAsync(bt => bt.Id == request.Id, cancellationToken); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/BugTypes/GetBugTypesHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Shared.Responses; 3 | using Mapster; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.Queries.BugTypes 12 | { 13 | public class GetBugTypesHandler : IRequestHandler> 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public GetBugTypesHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task> Handle(GetBugTypesQuery request, CancellationToken cancellationToken) 23 | { 24 | var bugTypes = await _context.BugTypes.AsNoTracking() 25 | .Where(x => x.ProjectId == request.ProjectId) 26 | .ProjectToType().ToListAsync(cancellationToken); 27 | 28 | return bugTypes; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Bugs/GetBugHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Shared.Responses; 3 | using Mapster; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace BugLab.Business.Queries.Bugs 10 | { 11 | public class GetBugHandler : IRequestHandler 12 | { 13 | private readonly AppDbContext _context; 14 | 15 | public GetBugHandler(AppDbContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | public Task Handle(GetBugQuery request, CancellationToken cancellationToken) 21 | { 22 | return _context.Bugs.AsNoTracking() 23 | .ProjectToType() 24 | .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Bugs/GetBugsHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Extensions; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Shared.Enums; 5 | using BugLab.Shared.Responses; 6 | using Mapster; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.Business.Queries.Bugs 14 | { 15 | public class GetBugsHandler : IRequestHandler> 16 | { 17 | private readonly AppDbContext _context; 18 | 19 | public GetBugsHandler(AppDbContext context) 20 | { 21 | _context = context; 22 | } 23 | 24 | public async Task> Handle(GetBugsQuery request, CancellationToken cancellationToken) 25 | { 26 | var query = _context.Bugs.AsNoTracking(); 27 | 28 | query = request.ProjectId.HasValue 29 | ? query.Where(b => b.ProjectId == request.ProjectId) 30 | : query.Where(b => b.CreatedById == request.UserId || b.AssignedToId == request.UserId); 31 | 32 | if (!string.IsNullOrWhiteSpace(request.Title)) query = query.Where(b => b.Title.Contains(request.Title)); 33 | 34 | var defaultOrder = query.OrderBy(b => b.Status); 35 | 36 | query = request.SortBy switch 37 | { 38 | BugSortBy.Title => defaultOrder.ThenSortBy(b => b.Title, request.SortOrder), 39 | _ => defaultOrder.ThenSortBy(b => b.Priority, request.SortOrder) 40 | }; 41 | 42 | return await PagedList.CreateAsync(query.ProjectToType(), 43 | request.PageNumber, request.PageSize, cancellationToken); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Projects/GetProjectHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Shared.Enums; 3 | using BugLab.Shared.Responses; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.Queries.Projects 12 | { 13 | public class GetProjectHandler : IRequestHandler 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public GetProjectHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task Handle(GetProjectQuery request, CancellationToken cancellationToken) 23 | { 24 | var project = await _context.Projects.ProjectToType().FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken); 25 | if (project == null) return project; 26 | 27 | var relatedBugsCount = await _context.Bugs.AsNoTracking() 28 | .Where(x => x.ProjectId == request.Id) 29 | .GroupBy(bug => 1, (key, bugs) => new 30 | { 31 | Total = bugs.Count(), 32 | HighPrioritized = bugs.Count(x => x.Priority == BugPriority.High) 33 | }).FirstOrDefaultAsync(cancellationToken); 34 | 35 | project.TotalBugs = relatedBugsCount.Total; 36 | project.TotalHighPriorityBugs = relatedBugsCount.HighPrioritized; 37 | return project; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Projects/GetProjectUsersHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Projects; 2 | using BugLab.Data; 3 | using BugLab.Shared.Responses; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.QueryHandlers.Projects 12 | { 13 | public class GetProjectUsersHandler : IRequestHandler> 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public GetProjectUsersHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task> Handle(GetProjectUsersQuery request, CancellationToken cancellationToken) 23 | { 24 | return await _context.ProjectUsers.Include(pu => pu.User).AsNoTracking() 25 | .Where(pu => pu.ProjectId == request.ProjectId) 26 | .Select(pu => new UserResponse { Email = pu.User.Email, Id = pu.User.Id }) 27 | .ToListAsync(cancellationToken); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Projects/GetProjectsHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Extensions; 2 | using BugLab.Business.Helpers; 3 | using BugLab.Data; 4 | using BugLab.Shared.Enums; 5 | using BugLab.Shared.Responses; 6 | using Mapster; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BugLab.Business.Queries.Projects 14 | { 15 | public class GetProjectsHandler : IRequestHandler> 16 | { 17 | private readonly AppDbContext _context; 18 | 19 | public GetProjectsHandler(AppDbContext context) 20 | { 21 | _context = context; 22 | } 23 | 24 | public async Task> Handle(GetProjectsQuery request, CancellationToken cancellationToken) 25 | { 26 | var query = _context.Projects.OrderBy(p => p.Title).AsNoTracking() 27 | .Where(x => _context.ProjectUsers.Where(pu => pu.UserId == request.UserId) 28 | .Select(pu => pu.ProjectId) 29 | .Contains(x.Id)); 30 | 31 | int totalItems; 32 | (query, totalItems) = await query.PaginateAsync(request.PageNumber, request.PageSize, cancellationToken); 33 | 34 | var relatedBugsCounts = await _context.Bugs.AsNoTracking() 35 | .Where(b => query.Select(p => p.Id).Contains(b.ProjectId)) 36 | .GroupBy(b => b.ProjectId, (key, bugs) => new 37 | { 38 | ProjectId = key, 39 | Total = bugs.Count(x => x.Status != BugStatus.Resolved), 40 | HighPrioritized = bugs.Count(x => x.Priority == BugPriority.High) 41 | }).ToListAsync(cancellationToken); 42 | 43 | var projects = await query.ProjectToType().ToListAsync(cancellationToken); 44 | projects = projects.Select(p => 45 | { 46 | var bugsCount = relatedBugsCounts.FirstOrDefault(x => x.ProjectId == p.Id); 47 | if (bugsCount == null) return p; 48 | 49 | p.TotalBugs = bugsCount.Total; 50 | p.TotalHighPriorityBugs = bugsCount.HighPrioritized; 51 | 52 | return p; 53 | }).ToList(); 54 | 55 | return new PagedList(projects, request.PageNumber, request.PageSize, totalItems); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Sprints/GetSprintHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Sprints; 2 | using BugLab.Data; 3 | using BugLab.Shared.Responses; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BugLab.Business.QueryHandlers.Sprints 11 | { 12 | public class GetSprintHandler : IRequestHandler 13 | { 14 | private readonly AppDbContext _context; 15 | 16 | public GetSprintHandler(AppDbContext context) 17 | { 18 | _context = context; 19 | } 20 | 21 | public async Task Handle(GetSprintQuery request, CancellationToken cancellationToken) 22 | { 23 | var sprints = await _context.Sprints.AsNoTracking() 24 | .ProjectToType() 25 | .FirstOrDefaultAsync(s => s.Id == request.Id); 26 | 27 | return sprints; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Sprints/GetSprintsHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Sprints; 2 | using BugLab.Data; 3 | using BugLab.Shared.Responses; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.Business.QueryHandlers.Sprints 13 | { 14 | public class GetSprintsHandler : IRequestHandler> 15 | { 16 | private readonly AppDbContext _context; 17 | 18 | public GetSprintsHandler(AppDbContext context) 19 | { 20 | _context = context; 21 | } 22 | 23 | public async Task> Handle(GetSprintsQuery request, CancellationToken cancellationToken) 24 | { 25 | var sprints = await _context.Sprints.AsNoTracking() 26 | .Where(s => s.ProjectId == request.ProjectId) 27 | .OrderByDescending(x => x.StartDate) 28 | .ProjectToType() 29 | .ToListAsync(cancellationToken); 30 | 31 | return sprints; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Users/GetDashboardHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Users; 2 | using BugLab.Data; 3 | using BugLab.Data.Entities; 4 | using BugLab.Shared.Enums; 5 | using BugLab.Shared.Responses; 6 | using Mapster; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Logging; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using System.Linq.Expressions; 16 | 17 | namespace BugLab.Business.QueryHandlers.Users 18 | { 19 | public class GetDashboardHandler : IRequestHandler 20 | { 21 | private readonly AppDbContext _context; 22 | private readonly ILogger _logger; 23 | 24 | public GetDashboardHandler(AppDbContext context, ILogger logger) 25 | { 26 | _context = context; 27 | _logger = logger; 28 | } 29 | 30 | public async Task Handle(GetDashboardQuery request, CancellationToken cancellationToken) 31 | { 32 | var dashboard = new DashboardResponse(); 33 | 34 | _logger.LogInformation("Getting project ids for dashboard"); 35 | var projectIds = await _context.ProjectUsers.Where(pu => pu.UserId == request.UserId) 36 | .Select(pu => pu.ProjectId).ToListAsync(cancellationToken); 37 | 38 | _logger.LogInformation("Getting latest bugs"); 39 | dashboard.LatestBug = await GetLatestBug(b => b.Created, projectIds, cancellationToken); 40 | dashboard.LatestUpdatedBug = await GetLatestBug(b => b.Modified, projectIds, cancellationToken); 41 | 42 | _logger.LogInformation("Getting bugs counts"); 43 | var bugsCounts = await _context.Bugs.AsNoTracking() 44 | .Where(b => projectIds.Contains(b.ProjectId)) 45 | .GroupBy(_ => 1, (_, bugs) => new 46 | { 47 | TotalOpenBugs = bugs.Count(b => b.Status == BugStatus.Open), 48 | TotalInProgressBugs = bugs.Count(b => b.Status == BugStatus.InProgress), 49 | TotalHighPrioritizedOpenBugs = bugs.Count(b => b.Priority == BugPriority.High), 50 | TotalBugsAssignedToMe = bugs.Count(b => b.AssignedToId == request.UserId) 51 | }).FirstOrDefaultAsync(cancellationToken); 52 | 53 | bugsCounts.Adapt(dashboard); 54 | 55 | return dashboard; 56 | } 57 | 58 | private async Task GetLatestBug(Expression> latestSelector, IEnumerable projectIds 59 | , CancellationToken cancellationToken) 60 | { 61 | return await _context.Bugs.AsNoTracking() 62 | .Where(b => projectIds.Contains(b.ProjectId)) 63 | .OrderByDescending(latestSelector) 64 | .ProjectToType() 65 | .FirstOrDefaultAsync(cancellationToken); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BugLab.Business/QueryHandlers/Users/GetUsersHandler.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Helpers; 2 | using BugLab.Data; 3 | using BugLab.Shared.Responses; 4 | using Mapster; 5 | using MediatR; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace BugLab.Business.Queries.Users 12 | { 13 | public class GetUsersHandler : IRequestHandler> 14 | { 15 | private readonly AppDbContext _context; 16 | 17 | public GetUsersHandler(AppDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | public async Task> Handle(GetUsersQuery request, CancellationToken cancellationToken) 23 | { 24 | var query = _context.Users.AsNoTracking(); 25 | 26 | if (!string.IsNullOrWhiteSpace(request.Email)) 27 | query = query.Where(x => x.Email.Contains(request.Email)); 28 | 29 | if (request.NotInProjectId.HasValue) 30 | { 31 | query = query.Where(u => !_context.ProjectUsers.Any(pu => pu.ProjectId == request.NotInProjectId && pu.UserId == u.Id)); 32 | } 33 | 34 | return await PagedList.CreateAsync(query.ProjectToType(), 35 | request.PageNumber, request.PageSize, cancellationToken); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BugLab.Business/Services/AuthService.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Interfaces; 2 | using BugLab.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace BugLab.Business.Services 9 | { 10 | public class AuthService : IAuthService 11 | { 12 | private readonly AppDbContext _context; 13 | 14 | public AuthService(AppDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public async Task HasAccessToProject(string userId, int projectId) 20 | { 21 | var userIsInProject = await _context.ProjectUsers.AnyAsync(pu => pu.ProjectId == projectId && pu.UserId == userId); 22 | 23 | if (!userIsInProject) 24 | { 25 | throw new UnauthorizedAccessException("You are not part of this project"); 26 | } 27 | } 28 | 29 | public async Task HasAccessToBug(string userId, int bugId) 30 | { 31 | var userIsInProject = await _context.ProjectUsers.AnyAsync(pu => pu.UserId == userId 32 | && pu.ProjectId == _context.Bugs.FirstOrDefault(b => b.Id == bugId).ProjectId); 33 | 34 | if (!userIsInProject) 35 | { 36 | throw new UnauthorizedAccessException("You are not part of this project"); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BugLab.Business/Services/EmailService.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Interfaces; 2 | using BugLab.Business.Options; 3 | using MailKit.Net.Smtp; 4 | using Microsoft.AspNetCore.WebUtilities; 5 | using Microsoft.Extensions.Options; 6 | using MimeKit; 7 | using MimeKit.Text; 8 | using System.IO; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace BugLab.Business.Services 13 | { 14 | public class EmailService : IEmailService 15 | { 16 | private readonly EmailOptions _emailOptions; 17 | private readonly ClientOptions _clientOptions; 18 | 19 | public EmailService(IOptions emailOptions, IOptions clientOptions) 20 | { 21 | _emailOptions = emailOptions.Value; 22 | _clientOptions = clientOptions.Value; 23 | } 24 | 25 | public async Task SendAsync(string subject, string body, string to) 26 | { 27 | var message = new MimeMessage(); 28 | message.From.Add(new MailboxAddress(_emailOptions.Name, _emailOptions.From)); 29 | message.To.Add(MailboxAddress.Parse(to)); 30 | message.Subject = subject; 31 | message.Body = new TextPart(TextFormat.Html) { Text = body }; 32 | 33 | using var client = new SmtpClient(); 34 | await client.ConnectAsync(_emailOptions.Host, _emailOptions.Port); 35 | await client.AuthenticateAsync(_emailOptions.From, _emailOptions.Password); 36 | await client.SendAsync(message); 37 | } 38 | 39 | public async Task SendEmailConfirmationAsync(string confirmationToken, string userId, string to) 40 | { 41 | var encodedToken = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(confirmationToken)); 42 | 43 | var template = await File.ReadAllTextAsync(@"Assets\email-template.html"); 44 | var link = $"{_clientOptions.Uri}/confirm-email/{userId}/token/{encodedToken}"; 45 | var body = template.Replace("{link}", link); 46 | 47 | await SendAsync("Confirm your email", body, to); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BugLab.Data/BugLab.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/AuditableEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System; 3 | 4 | namespace BugLab.Data.Entities 5 | { 6 | public abstract class AuditableEntity 7 | { 8 | public DateTime Created { get; set; } 9 | public DateTime? Modified { get; set; } 10 | public DateTime? Deleted { get; set; } 11 | 12 | public IdentityUser CreatedBy { get; set; } 13 | public string CreatedById { get; set; } 14 | 15 | public string ModifiedById { get; set; } 16 | public IdentityUser ModifiedBy { get; set; } 17 | 18 | public string DeletedById { get; set; } 19 | public IdentityUser DeletedBy { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /BugLab.Data/Entities/Bug.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | using Microsoft.AspNetCore.Identity; 3 | using System.Collections.Generic; 4 | 5 | namespace BugLab.Data.Entities 6 | { 7 | public class Bug : AuditableEntity 8 | { 9 | public int Id { get; init; } 10 | public string Title { get; set; } 11 | public string Description { get; set; } 12 | public BugPriority Priority { get; set; } 13 | public BugStatus Status { get; set; } 14 | 15 | public int ProjectId { get; init; } 16 | public Project Project { get; private set; } 17 | 18 | public int BugTypeId { get; set; } 19 | public BugType BugType { get; private set; } 20 | 21 | public string AssignedToId { get; set; } 22 | public IdentityUser AssignedTo { get; private set; } 23 | 24 | public Sprint Sprint { get; set; } 25 | public int? SprintId { get; set; } 26 | 27 | public ICollection Comments { get; init; } = new List(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/BugType.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Data.Entities 2 | { 3 | public class BugType 4 | { 5 | public int Id { get; set; } 6 | public int ProjectId { get; set; } 7 | public string Title { get; set; } 8 | public string Color { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/Comment.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Data.Entities 2 | { 3 | public class Comment : AuditableEntity 4 | { 5 | public int Id { get; set; } 6 | public string Text { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/Project.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Data.Entities 2 | { 3 | public class Project 4 | { 5 | public int Id { get; init; } 6 | public string Title { get; set; } 7 | public string Description { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/ProjectUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace BugLab.Data.Entities 4 | { 5 | public class ProjectUser 6 | { 7 | public int ProjectId { get; init; } 8 | public string UserId { get; init; } 9 | public IdentityUser User { get; init; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System; 3 | 4 | namespace BugLab.Data.Entities 5 | { 6 | public class RefreshToken 7 | { 8 | public int Id { get; set; } 9 | public string Value { get; init; } 10 | public DateTime ExpirationDate { get; init; } = DateTime.UtcNow.AddMonths(1); 11 | public IdentityUser User { get; init; } 12 | public string UserId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Data/Entities/Sprint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BugLab.Data.Entities 5 | { 6 | public class Sprint 7 | { 8 | public int Id { get; set; } 9 | public string Title { get; set; } 10 | public ICollection Bugs { get; set; } 11 | public Project Project { get; private set; } 12 | public int ProjectId { get; set; } 13 | public DateTime StartDate { get; set; } 14 | public DateTime EndDate { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/BugEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using BugLab.Data.Extensions; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace BugLab.Data.EntityConfigs 7 | { 8 | public class BugEntityConfig : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ConfigureAudit(); 13 | 14 | builder.HasIndex(x => x.Title); 15 | builder.HasIndex(x => x.Status); 16 | builder.HasIndex(x => x.Priority); 17 | 18 | builder.Property(x => x.Title) 19 | .IsRequired() 20 | .HasMaxLength(255); 21 | 22 | builder.HasMany(x => x.Comments) 23 | .WithOne() 24 | .IsRequired(); 25 | 26 | builder.HasOne(x => x.BugType) 27 | .WithMany() 28 | .HasForeignKey(x => x.BugTypeId) 29 | .IsRequired() 30 | .OnDelete(DeleteBehavior.Restrict); 31 | 32 | builder.HasOne(x => x.AssignedTo) 33 | .WithMany() 34 | .HasForeignKey(x => x.AssignedToId); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/BugTypeEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace BugLab.Data.EntityConfigs 6 | { 7 | public class BugTypeEntityConfig : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasOne() 12 | .WithMany() 13 | .HasForeignKey(x => x.ProjectId) 14 | .IsRequired(); 15 | 16 | builder.Property(x => x.Title) 17 | .HasMaxLength(255) 18 | .IsRequired(); 19 | 20 | builder.Property(x => x.Color) 21 | .HasMaxLength(25) 22 | .IsRequired(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/CommentEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using BugLab.Data.Extensions; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace BugLab.Data.EntityConfigs 7 | { 8 | public class CommentEntityConfig : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ConfigureAudit(); 13 | 14 | builder.ToTable("Comments"); 15 | 16 | builder.Property(x => x.Text) 17 | .IsRequired(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/ProjectEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace BugLab.Data.EntityConfigs 6 | { 7 | public class ProjectEntityConfig : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(x => x.Title) 12 | .IsRequired() 13 | .HasMaxLength(255); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/ProjectUserEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace BugLab.Data.EntityConfigs 6 | { 7 | public class ProjectUserEntityConfig : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(x => new { x.UserId, x.ProjectId }); 12 | 13 | builder.HasOne() 14 | .WithMany() 15 | .HasForeignKey(x => x.ProjectId); 16 | 17 | builder.HasOne(x => x.User) 18 | .WithMany() 19 | .HasForeignKey(x => x.UserId); 20 | 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BugLab.Data/EntityConfigs/SprintEntityConfig.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace BugLab.Data.EntityConfigs 6 | { 7 | public class SprintEntityConfig : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(x => x.Title) 12 | .IsRequired() 13 | .HasMaxLength(255); 14 | 15 | builder.HasMany(x => x.Bugs) 16 | .WithOne(x => x.Sprint) 17 | .OnDelete(DeleteBehavior.Restrict); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BugLab.Data/Extensions/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace BugLab.Data.Extensions 4 | { 5 | public static class ClaimsPrincipalExtensions 6 | { 7 | public static string UserId(this ClaimsPrincipal claimsPrincipal) 8 | { 9 | return claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BugLab.Data/Extensions/EntityConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace BugLab.Data.Extensions 6 | { 7 | public static class EntityConfigurationExtensions 8 | { 9 | public static void ConfigureAudit(this EntityTypeBuilder builder) where TEntity : AuditableEntity 10 | { 11 | builder.HasIndex(x => x.Deleted); 12 | 13 | builder.HasOne(x => x.CreatedBy) 14 | .WithMany() 15 | .IsRequired() 16 | .OnDelete(DeleteBehavior.Restrict) 17 | .HasForeignKey(x => x.CreatedById); 18 | 19 | builder.HasOne(x => x.ModifiedBy) 20 | .WithMany() 21 | .HasForeignKey(x => x.ModifiedById); 22 | 23 | builder.HasOne(x => x.DeletedBy) 24 | .WithMany() 25 | .HasForeignKey(x => x.DeletedById); 26 | 27 | builder.HasQueryFilter(x => !x.Deleted.HasValue); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Data/Extensions/EntityEntryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.ChangeTracking; 2 | using System; 3 | using System.Linq.Expressions; 4 | 5 | namespace BugLab.Data.Extensions 6 | { 7 | public static class EntityEntryExtensions 8 | { 9 | public static void SetProperty(this EntityEntry entry, 10 | Expression> propertyExpression, 11 | TProperty value) 12 | where TEntity : class 13 | { 14 | entry.Property(propertyExpression).CurrentValue = value; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Data/Extensions/ServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data.Helpers; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace BugLab.Data.Extensions 7 | { 8 | public static class ServicesExtensions 9 | { 10 | public static void AddDataServices(this IServiceCollection services, IConfiguration config) 11 | { 12 | services.AddDbContext(opt => opt.UseSqlServer(config.GetConnectionString("Default"))); 13 | services.AddSingleton(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /BugLab.Data/Helpers/DateProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BugLab.Data.Helpers 4 | { 5 | public class DateProvider : IDateProvider 6 | { 7 | public DateTime UtcDate => DateTime.UtcNow; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Data/Helpers/IDateProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BugLab.Data.Helpers 4 | { 5 | public interface IDateProvider 6 | { 7 | DateTime UtcDate { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Shared/BugLab.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /BugLab.Shared/Enums/BugEnums.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Enums 2 | { 3 | public enum BugPriority 4 | { 5 | None = 0, 6 | Low = 1, 7 | Medium = 2, 8 | High = 3 9 | } 10 | 11 | public enum BugStatus 12 | { 13 | Open = 0, 14 | InProgress = 1, 15 | Resolved = 2 16 | } 17 | 18 | public enum BugSortBy 19 | { 20 | Priority, 21 | Title 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BugLab.Shared/Enums/SortOrder.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Enums 2 | { 3 | public enum SortOrder 4 | { 5 | Ascending, 6 | Descending 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Headers/PaginationHeader.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Headers 2 | { 3 | public class PaginationHeader 4 | { 5 | public PaginationHeader(int pageNumber, int pageSize, int totalItems, int totalPages) 6 | { 7 | PageNumber = pageNumber; 8 | PageSize = pageSize; 9 | TotalItems = totalItems; 10 | TotalPages = totalPages; 11 | } 12 | 13 | public int PageNumber { get; init; } 14 | public int PageSize { get; init; } 15 | public int TotalItems { get; init; } 16 | public int TotalPages { get; init; } 17 | } 18 | } -------------------------------------------------------------------------------- /BugLab.Shared/Helpers/HttpClientHelpers/IKeyValueBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Helpers.HttpClientHelpers 2 | { 3 | public interface IKeyValueBuilder 4 | { 5 | IKeyValueBuilder WithParam(string key, string value); 6 | IKeyValueBuilder WithParam(string key, T value); 7 | string Build(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Shared/Helpers/HttpClientHelpers/QueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BugLab.Shared.Helpers.HttpClientHelpers 6 | { 7 | public class QueryBuilder : IKeyValueBuilder 8 | { 9 | private readonly StringBuilder _sb; 10 | 11 | protected QueryBuilder(string uri) 12 | { 13 | _sb = new StringBuilder(uri); 14 | } 15 | 16 | public IKeyValueBuilder WithParam(string key, string value) 17 | { 18 | if (string.IsNullOrWhiteSpace(value)) return this; 19 | Append(key, value); 20 | return this; 21 | } 22 | 23 | public IKeyValueBuilder WithParam(string key, T value) 24 | { 25 | if (EqualityComparer.Default.Equals(value, default)) return this; 26 | Append(key, $"{value}"); 27 | return this; 28 | } 29 | 30 | private void Append(string key, string value) 31 | { 32 | if (_sb[^1] != '?') _sb.Append('&'); 33 | 34 | _sb.Append(key) 35 | .Append('=') 36 | .Append(Uri.EscapeDataString(value)); 37 | } 38 | 39 | public static IKeyValueBuilder Use(string uri) 40 | { 41 | return new QueryBuilder(uri.EndsWith('?') ? uri : $"{uri}?"); 42 | } 43 | 44 | public string Build() 45 | { 46 | return _sb.ToString(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BugLab.Shared/QueryParams/BugParams.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | 3 | namespace BugLab.Shared.QueryParams 4 | { 5 | public class BugParams : PaginationParams 6 | { 7 | public int? ProjectId { get; set; } 8 | public BugSortBy SortBy { get; set; } 9 | public SortOrder SortOrder { get; set; } 10 | public string Title { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BugLab.Shared/QueryParams/PaginationParams.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.QueryParams 2 | { 3 | public class PaginationParams 4 | { 5 | private const int _min = 1; 6 | public const int MaxPageSize = 20; 7 | private int _pageSize = 10; 8 | private int _pageNumber = 1; 9 | 10 | public int PageNumber 11 | { 12 | get => _pageNumber; 13 | set => _pageNumber = value < _min ? _min : value; 14 | } 15 | 16 | public int PageSize 17 | { 18 | get => _pageSize; 19 | set => _pageSize = value is > MaxPageSize or < _min ? MaxPageSize : value; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /BugLab.Shared/QueryParams/UserParams.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.QueryParams 2 | { 3 | public class UserParams : PaginationParams 4 | { 5 | public string Email { get; set; } 6 | public int? NotInProjectId { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Auth/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.Auth 2 | { 3 | public class LoginRequest 4 | { 5 | private string _email; 6 | 7 | public string Email { get => _email; set => _email = value.Trim(); } 8 | public string Password { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Auth/RefreshTokenRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace BugLab.Shared.Requests.Auth 4 | { 5 | public class RefreshTokenRequest 6 | { 7 | [Required(AllowEmptyStrings = false)] 8 | public string RefreshToken { get; set; } 9 | 10 | [Required(AllowEmptyStrings = false)] 11 | public string AccessToken { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Auth/RegisterRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.Auth 2 | { 3 | public class RegisterRequest 4 | { 5 | private string _email; 6 | 7 | public string Email { get => _email; set => _email = value.Trim(); } 8 | public string Password { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/BugTypes/UpsertBugTypeRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.BugTypes 2 | { 3 | public class UpsertBugTypeRequest 4 | { 5 | public string Color { get; set; } 6 | public string Title { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Bugs/AddBugRequest.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | 3 | namespace BugLab.Shared.Requests.Bugs 4 | { 5 | public class AddBugRequest 6 | { 7 | public string Title { get; set; } 8 | public string Description { get; set; } 9 | public BugPriority Priority { get; set; } 10 | public BugStatus Status { get; set; } 11 | public int TypeId { get; set; } 12 | public int ProjectId { get; set; } 13 | public string AssignedToId { get; set; } 14 | public int? SprintId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Bugs/UpdateBugRequest.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | 3 | namespace BugLab.Shared.Requests.Bugs 4 | { 5 | public class UpdateBugRequest 6 | { 7 | public int Id { get; set; } 8 | public string Title { get; set; } 9 | public string Description { get; set; } 10 | public BugPriority Priority { get; set; } 11 | public BugStatus Status { get; set; } 12 | public int TypeId { get; set; } 13 | public string AssignedToId { get; set; } 14 | public int? SprintId { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Comments/UpsertCommentRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.Comments 2 | { 3 | public class UpsertCommentRequest 4 | { 5 | public string Text { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Projects/AddProjectRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.Projects 2 | { 3 | public class AddProjectRequest 4 | { 5 | public string Title { get; set; } 6 | public string Description { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Projects/UpdateProjectRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Requests.Projects 2 | { 3 | public class UpdateProjectRequest 4 | { 5 | public string Title { get; set; } 6 | public string Description { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Requests/Sprints/AddSprintRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BugLab.Shared.Requests.Sprints 4 | { 5 | public class AddSprintRequest 6 | { 7 | public string Title { get; set; } 8 | public DateTime StartDate { get; set; } 9 | public DateTime EndDate { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/ApiError.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Responses 2 | { 3 | public class ApiError 4 | { 5 | public ApiError(string message, string stackTrace = null) 6 | { 7 | Message = message; 8 | StackTrace = stackTrace; 9 | } 10 | 11 | public string StackTrace { get; } 12 | public string Message { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/BugResponse.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Enums; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace BugLab.Shared.Responses 6 | { 7 | public class BugResponse 8 | { 9 | public int Id { get; init; } 10 | public string Title { get; init; } 11 | public string Description { get; init; } 12 | public BugPriority Priority { get; init; } 13 | public BugStatus Status { get; set; } 14 | public DateTime Created { get; init; } 15 | public DateTime? Modified { get; init; } 16 | public string SprintTitle { get; init; } 17 | public int? SprintId { get; init; } 18 | public string ProjectTitle { get; init; } 19 | public int ProjectId { get; init; } 20 | public BugTypeResponse BugType { get; init; } 21 | public ICollection Comments { get; init; } = new List(); 22 | public UserResponse CreatedBy { get; init; } 23 | public UserResponse ModifiedBy { get; init; } 24 | public UserResponse AssignedTo { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/BugTypeResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Responses 2 | { 3 | public class BugTypeResponse 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } 7 | public string Color { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/CommentResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BugLab.Shared.Responses 4 | { 5 | public class CommentResponse 6 | { 7 | public int Id { get; set; } 8 | public string Text { get; set; } 9 | public DateTime Created { get; set; } 10 | public DateTime? Modified { get; set; } 11 | public UserResponse CreatedBy { get; set; } 12 | public UserResponse ModifiedBy { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /BugLab.Shared/Responses/DashboardResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Responses 2 | { 3 | public class DashboardResponse 4 | { 5 | public int TotalOpenBugs { get; set; } 6 | public int TotalInProgressBugs { get; set; } 7 | public int TotalHighPrioritizedOpenBugs { get; set; } 8 | public int TotalBugsAssignedToMe { get; set; } 9 | public BugResponse LatestBug { get; set; } 10 | public BugResponse LatestUpdatedBug { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/LoginResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BugLab.Shared.Responses 5 | { 6 | public class LoginResponse 7 | { 8 | public string Id { get; init; } 9 | public string Email { get; init; } 10 | public string Token { get; set; } 11 | public bool EmailConfirmed { get; init; } 12 | public string RefreshToken { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/ProjectResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Responses 2 | { 3 | public class ProjectResponse 4 | { 5 | public int Id { get; init; } 6 | public string Title { get; init; } 7 | public string Description { get; init; } 8 | public int TotalBugs { get; set; } 9 | public int TotalHighPriorityBugs { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/SprintDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BugLab.Shared.Responses 5 | { 6 | public class SprintDetailsResponse 7 | { 8 | public int Id { get; set; } 9 | public string Title { get; set; } 10 | public IEnumerable Bugs { get; set; } 11 | public string ProjectTitle { get; set; } 12 | public DateTime StartDate { get; set; } 13 | public DateTime EndDate { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/SprintForListResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BugLab.Shared.Responses 4 | { 5 | public class SprintForListResponse 6 | { 7 | public int Id { get; set; } 8 | public string Title { get; set; } 9 | public string ProjectTitle { get; set; } 10 | public DateTime StartDate { get; set; } 11 | public DateTime EndDate { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BugLab.Shared/Responses/UserResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BugLab.Shared.Responses 2 | { 3 | public class UserResponse 4 | { 5 | public string Id { get; set; } 6 | public string Email { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/AddBugValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Bugs; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class AddBugValidator : AbstractValidator 7 | { 8 | public AddBugValidator() 9 | { 10 | RuleFor(x => x.Title) 11 | .NotEmpty(); 12 | 13 | RuleFor(x => x.Priority) 14 | .IsInEnum(); 15 | 16 | RuleFor(x => x.Status) 17 | .IsInEnum(); 18 | 19 | RuleFor(x => x.ProjectId) 20 | .NotEmpty(); 21 | 22 | RuleFor(x => x.TypeId) 23 | .NotEmpty(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/AddProjectValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Projects; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class AddProjectValidator : AbstractValidator 7 | { 8 | public AddProjectValidator() 9 | { 10 | RuleFor(x => x.Title) 11 | .NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/AddSprintValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Sprints; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class AddSprintValidator : AbstractValidator 7 | { 8 | public AddSprintValidator() 9 | { 10 | RuleFor(x => x.Title).NotEmpty(); 11 | RuleFor(x => x.StartDate).NotEmpty(); 12 | RuleFor(x => x.EndDate).NotEmpty().GreaterThan(x => x.StartDate).WithMessage("Sprint cannot end before start date"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/LoginValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Auth; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class LoginValidator : AbstractValidator 7 | { 8 | public LoginValidator() 9 | { 10 | RuleFor(x => x.Email) 11 | .NotEmpty() 12 | .EmailAddress(); 13 | 14 | RuleFor(x => x.Password) 15 | .NotEmpty() 16 | .MinimumLength(6); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/RegisterValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Auth; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class RegisterValidator : AbstractValidator 7 | { 8 | public RegisterValidator() 9 | { 10 | RuleFor(x => x.Email) 11 | .NotEmpty() 12 | .EmailAddress(); 13 | 14 | RuleFor(x => x.Password) 15 | .NotEmpty() 16 | .MinimumLength(6); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/UpdateBugValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Bugs; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class UpdateBugValidator : AbstractValidator 7 | { 8 | public UpdateBugValidator() 9 | { 10 | RuleFor(x => x.Title) 11 | .NotEmpty(); 12 | 13 | RuleFor(x => x.Priority) 14 | .IsInEnum(); 15 | 16 | RuleFor(x => x.Status) 17 | .IsInEnum(); 18 | 19 | RuleFor(x => x.TypeId) 20 | .NotEmpty(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/UpdateProjectValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Projects; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class UpdateProjectValidator : AbstractValidator 7 | { 8 | public UpdateProjectValidator() 9 | { 10 | RuleFor(x => x.Title) 11 | .NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/UpsertBugTypeValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.BugTypes; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class UpsertBugTypeValidator : AbstractValidator 7 | { 8 | public UpsertBugTypeValidator() 9 | { 10 | RuleFor(x => x.Color) 11 | .NotEmpty(); 12 | 13 | RuleFor(x => x.Title) 14 | .NotEmpty(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BugLab.Shared/Validators/UpsertCommentValidator.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Requests.Comments; 2 | using FluentValidation; 3 | 4 | namespace BugLab.Shared.Validators 5 | { 6 | public class UpsertCommentValidator : AbstractValidator 7 | { 8 | public UpsertCommentValidator() 9 | { 10 | RuleFor(x => x.Text) 11 | .NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BugLab.Tests/BugLab.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/AddBugHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Bugs; 2 | using BugLab.Business.Commands.Bugs; 3 | using BugLab.Shared.Enums; 4 | using BugLab.Tests.Helpers; 5 | using FluentAssertions; 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace BugLab.Tests.Business.CommandHandlers 12 | { 13 | public class AddBugHandlerTests 14 | { 15 | private AddBugHandler _sut; 16 | 17 | [Fact] 18 | public async Task SetsCreatedTime_ToCurrentTime() 19 | { 20 | using var context = await DbContextHelpers.CreateAsync(); 21 | _sut = new(context); 22 | var command = new AddBugCommand("New Bug",null ,BugPriority.None, BugStatus.InProgress, 1, 1, null, null); 23 | var id = await _sut.Handle(command, default); 24 | 25 | id.Should().NotBe(0); 26 | var addedBug = context.Bugs.OrderBy(x => x.Id).LastOrDefault(); 27 | addedBug.Created.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(5)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/AddCommentHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Comments; 2 | using BugLab.Business.Commands.Comments; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace BugLab.Tests.Business.CommandHandlers 12 | { 13 | public class AddCommentHandlerTests 14 | { 15 | private AddCommentHandler _sut; 16 | private AddCommentCommand _command; 17 | 18 | [Fact] 19 | public async Task AddsCommentToBug() 20 | { 21 | using var context = await DbContextHelpers.CreateAsync(); 22 | _sut = new(context); 23 | _command = new(1, Guid.NewGuid().ToString()); 24 | 25 | await _sut.Handle(_command, default); 26 | 27 | var bug = context.Bugs.First(b => b.Id == _command.BugId); 28 | bug.Comments.Should().Contain(c => c.Text == _command.Text); 29 | } 30 | 31 | [Fact] 32 | public async Task ThrowsNotFound_If_BugDoesNotExist() 33 | { 34 | using var context = await DbContextHelpers.CreateAsync(); 35 | _sut = new(context); 36 | _command = new AddCommentCommand(int.MaxValue, Guid.NewGuid().ToString()); 37 | 38 | await _sut.Invoking(_ => _.Handle(_command, default)) 39 | .Should().ThrowAsync(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/AddProjectUsersHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Projects; 2 | using BugLab.Business.Commands.Projects; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using System; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace BugLab.Tests.Business.CommandHandlers 10 | { 11 | public class AddProjectUsersHandlerTests 12 | { 13 | private AddProjectUsersHandler _sut; 14 | private AddProjectUsersCommand _command; 15 | 16 | [Fact] 17 | public async Task ThrowsException_WhenTryingToAdd_AlreadyExistingUser() 18 | { 19 | using var context = await DbContextHelpers.CreateAsync(); 20 | _sut = new(context); 21 | _command = new(1, new string[] { DbContextHelpers.CurrentUserId }); 22 | 23 | await _sut.Invoking(_ => _.Handle(_command, default)) 24 | .Should().ThrowAsync(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/DeleteBugHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Bugs; 2 | using BugLab.Business.Commands.Bugs; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace BugLab.Tests.Business.CommandHandlers 12 | { 13 | public class DeleteBugHandlerTests 14 | { 15 | private DeleteBugHandler _sut; 16 | private DeleteBugCommand _command = new(1); 17 | 18 | [Fact] 19 | public async Task SoftDeletes() 20 | { 21 | using var context = await DbContextHelpers.CreateAsync(); 22 | _sut = new(context); 23 | await _sut.Handle(_command, default); 24 | 25 | var deletedBug = context.Bugs.IgnoreQueryFilters().First(x => x.Id == _command.Id); 26 | deletedBug.Should().NotBeNull(); 27 | deletedBug.Deleted.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromMinutes(5)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/DeleteBugTypeHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.BugTypes; 2 | using BugLab.Business.Commands.BugTypes; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace BugLab.Tests.Business.CommandHandlers 11 | { 12 | public class DeleteBugTypeHandlerTests 13 | { 14 | private DeleteBugTypeHandler _sut; 15 | private DeleteBugTypeCommand _command; 16 | 17 | [Fact] 18 | public async Task Throws_WhenBugType_IsNotFound() 19 | { 20 | using var context = await DbContextHelpers.CreateAsync(); 21 | _sut = new(context); 22 | _command = new(int.MaxValue); 23 | 24 | await _sut.Invoking(_ => _.Handle(_command, default)) 25 | .Should().ThrowAsync(); 26 | } 27 | 28 | [Fact] 29 | public async Task Throws_WhenBugType_IsUsedByOtherBugs() 30 | { 31 | using var context = await DbContextHelpers.CreateAsync(); 32 | _sut = new(context); 33 | _command = new(1); 34 | 35 | await _sut.Invoking(_ => _.Handle(_command, default)) 36 | .Should().ThrowAsync(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/DeleteProjectHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Projects; 2 | using BugLab.Business.Commands.Projects; 3 | using BugLab.Data.Entities; 4 | using BugLab.Shared.Enums; 5 | using BugLab.Tests.Helpers; 6 | using FluentAssertions; 7 | using Microsoft.EntityFrameworkCore; 8 | using System; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Xunit; 12 | 13 | namespace BugLab.Tests.Business.CommandHandlers 14 | { 15 | public class DeleteProjectHandlerTests 16 | { 17 | private DeleteProjectHandler _sut; 18 | private DeleteProjectCommand _command; 19 | 20 | [Fact] 21 | public async Task Throws_WhenHasRelatedNonResolvedBugs() 22 | { 23 | using var context = await DbContextHelpers.CreateAsync(); 24 | var projectId = (await context.Bugs.FirstAsync(x => x.Status != BugStatus.Resolved)).ProjectId; 25 | _sut = new DeleteProjectHandler(context); 26 | 27 | await _sut.Invoking(_ => _.Handle(new DeleteProjectCommand(projectId), default)) 28 | .Should().ThrowAsync(); 29 | } 30 | 31 | [Fact] 32 | public async Task RemovesRelatedBugs_ThatAreResolvedOrDeleted() 33 | { 34 | using var context = await DbContextHelpers.CreateAsync(); 35 | var project = (await context.AddSeedData(new Project { Title = "new project" }))[0]; 36 | 37 | await context.AddSeedData( 38 | new Bug { Title = "bug", BugTypeId = 1, Status = BugStatus.Resolved, ProjectId = project.Id, CreatedById = DbContextHelpers.CurrentUserId }, 39 | new Bug { Title = "bug", BugTypeId = 1, Deleted = DateTime.UtcNow, CreatedById = DbContextHelpers.CurrentUserId, ProjectId = project.Id }); 40 | 41 | _sut = new DeleteProjectHandler(context); 42 | await _sut.Handle(new DeleteProjectCommand(project.Id), default); 43 | 44 | context.Bugs.IgnoreQueryFilters().Where(x => x.ProjectId == project.Id).ToList() 45 | .Should().BeEmpty(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/CommandHandlers/DeleteProjectUserHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.CommandHandlers.Projects; 2 | using BugLab.Business.Commands.Projects; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace BugLab.Tests.Business.CommandHandlers 10 | { 11 | public class DeleteProjectUserHandlerTests 12 | { 13 | private DeleteProjectUserHandler _sut; 14 | private DeleteProjectUserCommand _command; 15 | 16 | [Fact] 17 | public async Task RemovesUser_FromProject() 18 | { 19 | int projectId = 1; 20 | using var context = await DbContextHelpers.CreateAsync(); 21 | _sut = new(context); 22 | _command = new(projectId, DbContextHelpers.CurrentUserId); 23 | 24 | await _sut.Handle(_command, default); 25 | 26 | context.ProjectUsers.Should().NotContain(pu => pu.UserId == DbContextHelpers.CurrentUserId && pu.ProjectId == projectId); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/Extensions/IQueryableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Extensions; 2 | using BugLab.Tests.Helpers; 3 | using FluentAssertions; 4 | using Microsoft.Azure.Cosmos.Linq; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace BugLab.Tests.Business.Extensions 9 | { 10 | public class IQueryableExtensionsTests 11 | { 12 | [Fact] 13 | public async Task PaginateAsync_Returns_AmountOfTotalItems() 14 | { 15 | var context = await DbContextHelpers.CreateAsync(); 16 | var expectedBugCount = await context.Bugs.CountAsync(); 17 | 18 | var (query, bugCount) = await context.Bugs.PaginateAsync(1, 1, default); 19 | 20 | bugCount.Should().Be(expectedBugCount); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/QueryHandlers/GetBugsHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Bugs; 2 | using BugLab.Data.Entities; 3 | using BugLab.Shared.Responses; 4 | using BugLab.Tests.Helpers; 5 | using FluentAssertions; 6 | using Microsoft.EntityFrameworkCore; 7 | using System; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | 12 | namespace BugLab.Tests.Business.QueryHandlers 13 | { 14 | public class GetBugsHandlerTests 15 | { 16 | private GetBugsHandler _sut; 17 | 18 | private bool AssignedToOrCreatedBy(BugResponse bug, string userId) 19 | { 20 | if (bug.CreatedBy.Id == userId) return true; 21 | 22 | var assignedToId = bug.AssignedTo?.Id; 23 | return assignedToId != null && assignedToId == userId; 24 | } 25 | 26 | [Fact] 27 | public async Task GetBugs_ShouldNot_ReturnDeletedBugs() 28 | { 29 | using var context = await DbContextHelpers.CreateAsync(); 30 | _sut = new(context); 31 | 32 | var deletedBug = context.Bugs.IgnoreQueryFilters().FirstOrDefault(x => x.Deleted.HasValue); 33 | var bugs = await _sut.Handle(new GetBugsQuery(DbContextHelpers.CurrentUserId), default); 34 | 35 | bugs.Should().NotBeNullOrEmpty(); 36 | bugs.Should().NotContain(x => x.Id == deletedBug.Id); 37 | } 38 | 39 | [Fact] 40 | public async Task GetBugs_WithUserId_ReturnsUsersBugs() 41 | { 42 | using var context = await DbContextHelpers.CreateAsync(); 43 | _sut = new(context); 44 | 45 | var bugs = await _sut.Handle(new GetBugsQuery(DbContextHelpers.CurrentUserId), default); 46 | 47 | bugs.Should().NotBeNullOrEmpty(); 48 | bugs.Should().Match(b => b.All(b => AssignedToOrCreatedBy(b, DbContextHelpers.CurrentUserId))); 49 | } 50 | 51 | [Fact] 52 | public async Task GetBugs_WithProjectId_ReturnsBugsInProject() 53 | { 54 | using var context = await DbContextHelpers.CreateAsync(); 55 | _sut = new(context); 56 | 57 | var request = new GetBugsQuery(DbContextHelpers.CurrentUserId); 58 | request.ProjectId = context.ProjectUsers.AsNoTracking().First(x => x.UserId == DbContextHelpers.CurrentUserId).ProjectId; 59 | var bugs = await _sut.Handle(request, default); 60 | 61 | bugs.Should().NotBeNullOrEmpty(); 62 | bugs.Should().Match(b => b.All(b => b.ProjectId == request.ProjectId)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/QueryHandlers/GetProjectHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Queries.Projects; 2 | using BugLab.Data.Entities; 3 | using BugLab.Tests.Helpers; 4 | using FluentAssertions; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace BugLab.Tests.Business.QueryHandlers 10 | { 11 | public class GetProjectHandlerTests 12 | { 13 | private GetProjectHandler _sut; 14 | 15 | [Fact] 16 | public async Task GetProject_ReturnsNull_WhenNotFound() 17 | { 18 | using var context = await DbContextHelpers.CreateAsync(); 19 | _sut = new(context); 20 | var result = await _sut.Handle(new GetProjectQuery(default), default); 21 | 22 | result.Should().BeNull(); 23 | } 24 | 25 | [Fact] 26 | public async Task GetProject_ReturnsSpecifiedProject_With_BugsCount() 27 | { 28 | using var context = await DbContextHelpers.CreateAsync(); 29 | _sut = new(context); 30 | var projectId = context.Projects.FirstOrDefault().Id; 31 | var result = await _sut.Handle(new GetProjectQuery(projectId), default); 32 | 33 | result.Should().NotBeNull(); 34 | result.Id.Should().Be(projectId); 35 | result.TotalBugs.Should().BeGreaterThan(0); 36 | result.TotalHighPriorityBugs.Should().BeGreaterThan(0); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BugLab.Tests/Business/Services/AuthServiceTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Business.Services; 2 | using BugLab.Tests.Helpers; 3 | using FluentAssertions; 4 | using System; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace BugLab.Tests.Business.Services 9 | { 10 | public class AuthServiceTests 11 | { 12 | private AuthService _sut; 13 | 14 | [Fact] 15 | public async Task HasAccessToProject_OnlyThrowsException_WhenUserIsNotInProject() 16 | { 17 | using var context = await DbContextHelpers.CreateAsync(); 18 | _sut = new(context); 19 | 20 | await _sut.Invoking(_ => _.HasAccessToProject(Guid.NewGuid().ToString(), 1)) 21 | .Should().ThrowAsync(); 22 | 23 | await _sut.Invoking(_ => _.HasAccessToProject(DbContextHelpers.CurrentUserId, 1)) 24 | .Should().NotThrowAsync(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BugLab.Tests/Helpers/DbContextHelpers.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Data.Helpers; 3 | using Microsoft.AspNetCore.Http; 4 | using Moq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using TestSupport.EfHelpers; 8 | 9 | namespace BugLab.Tests.Helpers 10 | { 11 | public static class DbContextHelpers 12 | { 13 | public static string CurrentUserId { get; private set; } 14 | 15 | public static async Task CreateAsync(string currentUserId = "757b2158-40c3-4917-9523-5861973a4d2e", IDateProvider dateProvider = null) 16 | { 17 | CurrentUserId = currentUserId; 18 | var options = SqliteInMemory.CreateOptions(); 19 | 20 | if (dateProvider == null) dateProvider = new DateProvider(); 21 | 22 | var httpAccessor = CreateHttpAccessor(); 23 | var context = new AppDbContext(options, dateProvider, httpAccessor); 24 | context.Database.EnsureCreated(); 25 | context.SeedBugs(); 26 | 27 | await context.SaveChangesAsync(); 28 | 29 | context.ChangeTracker.Clear(); 30 | return context; 31 | } 32 | 33 | private static IHttpContextAccessor CreateHttpAccessor() 34 | { 35 | var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.NameIdentifier, CurrentUserId) })); 36 | var mockHttpAccessor = new Mock(); 37 | mockHttpAccessor.Setup(_ => _.HttpContext.User).Returns(claimsPrincipal); 38 | 39 | return mockHttpAccessor.Object; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BugLab.Tests/Helpers/Seed.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Data; 2 | using BugLab.Data.Entities; 3 | using BugLab.Shared.Enums; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace BugLab.Tests.Helpers 8 | { 9 | public static class Seed 10 | { 11 | public static void SeedBugs(this AppDbContext context) 12 | { 13 | context.Bugs.AddRange( 14 | new Bug { Title = "bug1", Priority = BugPriority.High, Status = BugStatus.Open, ProjectId = 1, BugTypeId = 1 }, 15 | new Bug { Title = "bug2", ProjectId = 1, BugTypeId = 1, Deleted = DateTime.UtcNow }, 16 | new Bug { Title = "bug2", Status = BugStatus.Resolved, ProjectId = 1, BugTypeId = 1, } 17 | ); 18 | } 19 | 20 | public static async Task AddSeedData(this AppDbContext context, params T[] data) where T : class 21 | { 22 | await context.Set().AddRangeAsync(data); 23 | await context.SaveChangesAsync(); 24 | context.ChangeTracker.Clear(); 25 | return data; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BugLab.Tests/Shared/QueryBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using BugLab.Shared.Helpers.HttpClientHelpers; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace BugLab.Tests.Shared 6 | { 7 | public class QueryBuilderTests 8 | { 9 | [Fact] 10 | public void WithParam_EscapesValues_WithPercentageSign_In_Sentence() 11 | { 12 | var hello = "hello"; 13 | var world = "world"; 14 | 15 | var result = QueryBuilder.Use("api") 16 | .WithParam("sentence", $"{hello} {world}") 17 | .Build(); 18 | 19 | result.Should().Contain("%"); 20 | var splittedResult = result.Split('%'); 21 | splittedResult[^1].Should().ContainAll(world, "20"); 22 | splittedResult[^2].Should().Contain(hello); 23 | } 24 | } 25 | } 26 | --------------------------------------------------------------------------------