├── .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