From b4e428a9cdf909f0208ceb777aeb75cfeaf06873 Mon Sep 17 00:00:00 2001 From: Nickolas Gupton Date: Sat, 28 Dec 2024 19:54:57 -0600 Subject: [PATCH] Initiative Dashboard and start on WorkItem Queues --- Core/Bones.Database/BonesDatabaseModule.cs | 3 - .../Migrations/20241003032709_Initial.cs | 4 +- .../Migrations/20241003042835_Rename.cs | 3 +- .../SetEmailConfirmedDateTimeDb.cs | 39 ++++ .../SetEmailConfirmedDateTimeDbCommand.cs | 10 - ...mailConfirmedDateTimeDbCommandValidator.cs | 6 - .../SetEmailConfirmedDateTimeDbHandler.cs | 14 -- .../GetOrganizationByIdDb.cs | 34 +++ .../GetOrganizationByIdDbHandler.cs | 18 -- .../GetOrganizationByIdDbQuery.cs | 9 - .../GetOrganizationByIdDbQueryValidator.cs | 11 - .../Initiatives/GetInitiativeByIdDb.cs | 26 +++ .../Initiatives/GetInitiativesByProjectDb.cs | 11 +- .../GetWorkItemQueuesByInitiativeDb.cs | 24 ++ Core/Bones.Logic/BonesBackendModule.cs | 3 - ...ConfirmEmailHandler.cs => ConfirmEmail.cs} | 30 ++- .../ConfirmEmail/ConfirmEmailQuery.cs | 11 - .../ConfirmEmailQueryValidator.cs | 6 - ...Handler.cs => GetUserByClaimsPrincipal.cs} | 17 +- .../GetUserByClaimsPrincipalQuery.cs | 10 - .../GetUserByClaimsPrincipalQueryValidator.cs | 13 -- ...ilHandler.cs => QueueConfirmationEmail.cs} | 24 +- .../QueueConfirmationEmailCommand.cs | 11 - .../QueueConfirmationEmailCommandValidator.cs | 20 -- ...Handler.cs => QueueForgotPasswordEmail.cs} | 23 +- .../QueueForgotPasswordEmailCommand.cs | 9 - ...ueueForgotPasswordEmailCommandValidator.cs | 20 -- .../Accounts/QueueResendConfirmationEmail.cs | 40 ++++ .../QueueResendConfirmationEmailCommand.cs | 7 - ...ResendConfirmationEmailCommandValidator.cs | 6 - .../QueueResendConfirmationEmailHandler.cs | 20 -- ...rUserQueryValidator.cs => RegisterUser.cs} | 42 +++- .../RegisterUser/RegisterUserHandler.cs | 37 ---- .../RegisterUser/RegisterUserQuery.cs | 10 - .../Features/Accounts/ResetPassword.cs | 21 ++ .../ResetPassword/ResetPasswordCommand.cs | 6 - .../ResetPasswordCommandValidator.cs | 6 - .../ResetPassword/ResetPasswordHandler.cs | 9 - ...or.cs => UserHasOrganizationPermission.cs} | 16 +- .../UserHasOrganizationPermissionHandler.cs | 41 ---- .../UserHasOrganizationPermissionQuery.cs | 11 - .../Projects/Initiatives/CreateInitiative.cs | 4 +- .../Projects/Initiatives/GetInitiativeById.cs | 16 +- .../Initiatives/GetInitiativesByProject.cs | 14 +- .../Initiatives/QueueDeleteInitiativeById.cs | 34 +++ .../QueueDeleteInitiativeByIdCommand.cs | 10 - ...eueDeleteInitiativeByIdCommandValidator.cs | 6 - .../QueueDeleteInitiativeByIdHandler.cs | 12 - .../UserHasInitiativePermission.cs | 75 +++++++ ...eateProjectHandler.cs => CreateProject.cs} | 20 +- .../CreateProject/CreateProjectCommand.cs | 11 - .../CreateProjectCommandValidator.cs | 6 - ...rojectByIdHandler.cs => GetProjectById.cs} | 19 +- .../GetProjectById/GetProjectByIdQuery.cs | 10 - .../GetProjectByIdQueryValidator.cs | 13 -- ...yOwnerHandler.cs => GetProjectsByOwner.cs} | 21 +- .../GetProjectsByOwnerQuery.cs | 12 - .../GetProjectsByOwnerQueryValidator.cs | 15 -- ...Handler.cs => GetProjectsUserCanAccess.cs} | 19 +- .../GetProjectsUserCanAccessQuery.cs | 9 - .../GetProjectsUserCanAccessQueryValidator.cs | 13 -- ...Handler.cs => UserHasProjectPermission.cs} | 28 ++- .../UserHasProjectPermissionQuery.cs | 11 - .../UserHasProjectPermissionQueryValidator.cs | 21 -- .../Queues/CreateQueue/CreateQueueCommand.cs | 11 - .../CreateQueueCommandValidator.cs | 6 - .../Queues/CreateQueue/CreateQueueHandler.cs | 12 - .../Projects/WorkItems/CreateWorkItemQueue.cs | 26 +++ .../GetWorkItemQueuesByInitiative.cs | 37 ++++ .../Models/QueryResponse.cs | 1 - Core/Bones.Shared/Consts/BonesClaimTypes.cs | 14 +- Core/Bones.Shared/Consts/FrontEndUrls.cs | 19 ++ .../Bones.Api.Client/AutoGenBonesApiClient.cs | 207 +++++++++++++++++- Frontend/Bones.WebUI/Bones.WebUI.csproj | 6 +- .../Pages/Account/MyProfilePage.razor.cs | 8 +- .../Project/CreateInitiativePage.razor.cs | 2 - .../Pages/Project/CreateProjectPage.razor.cs | 2 - .../CreateWorkItemQueueInInitiativePage.razor | 30 +++ ...eateWorkItemQueueInInitiativePage.razor.cs | 65 ++++++ .../Project/InitiativeDashboardPage.razor | 20 +- .../Project/InitiativeDashboardPage.razor.cs | 19 +- .../Project/ProjectDashboardPage.razor.cs | 22 +- .../Controllers/AnonymousController.cs | 5 +- .../Controllers/BonesControllerBase.cs | 2 +- .../Bones.Api/Controllers/LoginController.cs | 1 - .../ProjectController.Responses.cs | 62 ++++++ .../Controllers/ProjectController.cs | 50 ++++- .../Controllers/SysAdminController.cs | 3 +- Services/Bones.Api/OpenApi/swagger.json | 162 +++++++++++++- Services/Bones.BackgroundService/Program.cs | 3 - .../QueueConfirmationEmailTests.cs | 3 +- .../QueueForgotPasswordEmailTests.cs | 3 +- .../AccountManagement/RegisterUserTests.cs | 2 +- 93 files changed, 1263 insertions(+), 620 deletions(-) create mode 100644 Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb.cs delete mode 100644 Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommand.cs delete mode 100644 Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommandValidator.cs delete mode 100644 Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbHandler.cs create mode 100644 Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb.cs delete mode 100644 Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbHandler.cs delete mode 100644 Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQuery.cs delete mode 100644 Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQueryValidator.cs create mode 100644 Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativeByIdDb.cs create mode 100644 Core/Bones.Database/Operations/WorkItemManagement/WorkItemQueues/GetWorkItemQueuesByInitiativeDb.cs rename Core/Bones.Logic/Features/Accounts/{ConfirmEmail/ConfirmEmailHandler.cs => ConfirmEmail.cs} (59%) delete mode 100644 Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQuery.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQueryValidator.cs rename Core/Bones.Logic/Features/Accounts/{GetUserByClaimsPrincipal/GetUserByClaimsPrincipalHandler.cs => GetUserByClaimsPrincipal.cs} (50%) delete mode 100644 Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQuery.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQueryValidator.cs rename Core/Bones.Logic/Features/Accounts/{QueueConfirmationEmail/QueueConfirmationEmailHandler.cs => QueueConfirmationEmail.cs} (64%) delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommand.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommandValidator.cs rename Core/Bones.Logic/Features/Accounts/{QueueForgotPasswordEmail/QueueForgotPasswordEmailHandler.cs => QueueForgotPasswordEmail.cs} (66%) delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommand.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommandValidator.cs create mode 100644 Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommand.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommandValidator.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailHandler.cs rename Core/Bones.Logic/Features/Accounts/{RegisterUser/RegisterUserQueryValidator.cs => RegisterUser.cs} (55%) delete mode 100644 Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserHandler.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQuery.cs create mode 100644 Core/Bones.Logic/Features/Accounts/ResetPassword.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommand.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommandValidator.cs delete mode 100644 Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordHandler.cs rename Core/Bones.Logic/Features/Organizations/{UserHasOrganizationPermission/UserHasOrganizationPermissionQueryValidator.cs => UserHasOrganizationPermission.cs} (52%) delete mode 100644 Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionHandler.cs delete mode 100644 Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQuery.cs create mode 100644 Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommand.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommandValidator.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdHandler.cs create mode 100644 Core/Bones.Logic/Features/Projects/Initiatives/UserHasInitiativePermission.cs rename Core/Bones.Logic/Features/Projects/Projects/{CreateProject/CreateProjectHandler.cs => CreateProject.cs} (65%) delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommand.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommandValidator.cs rename Core/Bones.Logic/Features/Projects/Projects/{GetProjectById/GetProjectByIdHandler.cs => GetProjectById.cs} (67%) delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQuery.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQueryValidator.cs rename Core/Bones.Logic/Features/Projects/Projects/{GetProjectsByOwner/GetProjectsByOwnerHandler.cs => GetProjectsByOwner.cs} (64%) delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQueryValidator.cs rename Core/Bones.Logic/Features/Projects/Projects/{GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs => GetProjectsUserCanAccess.cs} (64%) delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs rename Core/Bones.Logic/Features/Projects/Projects/{UserHasProjectPermission/UserHasProjectPermissionHandler.cs => UserHasProjectPermission.cs} (65%) delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQuery.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQueryValidator.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommand.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommandValidator.cs delete mode 100644 Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueHandler.cs create mode 100644 Core/Bones.Logic/Features/Projects/WorkItems/CreateWorkItemQueue.cs create mode 100644 Core/Bones.Logic/Features/Projects/WorkItems/GetWorkItemQueuesByInitiative.cs create mode 100644 Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor create mode 100644 Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor.cs diff --git a/Core/Bones.Database/BonesDatabaseModule.cs b/Core/Bones.Database/BonesDatabaseModule.cs index 32a2681..6123bac 100644 --- a/Core/Bones.Database/BonesDatabaseModule.cs +++ b/Core/Bones.Database/BonesDatabaseModule.cs @@ -1,9 +1,6 @@ using Autofac; using Bones.Database.Models; -using Bones.Shared.Backend.PipelineBehaviors; using Bones.Shared.Exceptions; -using MediatR.Extensions.Autofac.DependencyInjection; -using MediatR.Extensions.Autofac.DependencyInjection.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/Core/Bones.Database/Migrations/20241003032709_Initial.cs b/Core/Bones.Database/Migrations/20241003032709_Initial.cs index db8ecca..da4f5c8 100644 --- a/Core/Bones.Database/Migrations/20241003032709_Initial.cs +++ b/Core/Bones.Database/Migrations/20241003032709_Initial.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable diff --git a/Core/Bones.Database/Migrations/20241003042835_Rename.cs b/Core/Bones.Database/Migrations/20241003042835_Rename.cs index 89641a9..6a4a304 100644 --- a/Core/Bones.Database/Migrations/20241003042835_Rename.cs +++ b/Core/Bones.Database/Migrations/20241003042835_Rename.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb.cs b/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb.cs new file mode 100644 index 0000000..03502a3 --- /dev/null +++ b/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb.cs @@ -0,0 +1,39 @@ +using Bones.Database.DbSets.AccountManagement; + +namespace Bones.Database.Operations.AccountManagement; + +/// +/// Sets the email confirmed date time on the user +/// +/// +/// +public sealed record SetEmailConfirmedDateTimeDbCommand(BonesUser User, DateTimeOffset ConfirmedDateTime) : IRequest; + +internal sealed class SetEmailConfirmedDateTimeDbCommandValidator : AbstractValidator +{ + public SetEmailConfirmedDateTimeDbCommandValidator() + { + RuleFor(x => x.User).NotNull().Custom((user, ctx) => + { + if (user.EmailConfirmed) + { + ctx.AddFailure("User", "User already has email confirmed"); + } + }); + + RuleFor(x => x.ConfirmedDateTime).NotNull(); + } +} + +internal sealed class SetEmailConfirmedDateTimeDbHandler(BonesDbContext dbContext) : IRequestHandler +{ + public async Task Handle(SetEmailConfirmedDateTimeDbCommand request, CancellationToken cancellationToken) + { + request.User.EmailConfirmed = true; + request.User.EmailConfirmedDateTime = request.ConfirmedDateTime; + dbContext.Users.Update(request.User); + await dbContext.SaveChangesAsync(cancellationToken); + + return CommandResponse.Pass(); + } +} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommand.cs b/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommand.cs deleted file mode 100644 index dfb68c5..0000000 --- a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Database.Operations.AccountManagement.SetEmailConfirmedDateTimeDb; - -/// -/// Sets the email confirmed date time on the user -/// -/// -/// -public sealed record SetEmailConfirmedDateTimeDbCommand(BonesUser User, DateTimeOffset ConfirmedDateTime) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommandValidator.cs b/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommandValidator.cs deleted file mode 100644 index 2866bc4..0000000 --- a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Database.Operations.AccountManagement.SetEmailConfirmedDateTimeDb; - -internal sealed class SetEmailConfirmedDateTimeDbCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbHandler.cs b/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbHandler.cs deleted file mode 100644 index d93f27d..0000000 --- a/Core/Bones.Database/Operations/AccountManagement/SetEmailConfirmedDateTimeDb/SetEmailConfirmedDateTimeDbHandler.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Bones.Database.Operations.AccountManagement.SetEmailConfirmedDateTimeDb; - -internal sealed class SetEmailConfirmedDateTimeDbHandler(BonesDbContext dbContext) : IRequestHandler -{ - public async Task Handle(SetEmailConfirmedDateTimeDbCommand request, CancellationToken cancellationToken) - { - request.User.EmailConfirmed = true; - request.User.EmailConfirmedDateTime = request.ConfirmedDateTime; - dbContext.Users.Update(request.User); - await dbContext.SaveChangesAsync(cancellationToken); - - return CommandResponse.Pass(); - } -} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb.cs b/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb.cs new file mode 100644 index 0000000..521ad8f --- /dev/null +++ b/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb.cs @@ -0,0 +1,34 @@ +using Bones.Database.DbSets.OrganizationManagement; + +namespace Bones.Database.Operations.OrganizationManagement; + +/// +/// DB Query to get the specified organization. +/// +/// +public sealed record GetOrganizationByIdDbQuery(Guid OrganizationId) : IRequest>; + +internal sealed class GetOrganizationByIdDbQueryValidator : AbstractValidator +{ + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) + { + RuleFor(x => x.OrganizationId).NotNull().NotEqual(Guid.Empty); + + return base.ValidateAsync(context, cancellation); + } +} + +internal sealed class GetOrganizationByIdDbHandler(BonesDbContext dbContext) : IRequestHandler> +{ + public async Task> Handle(GetOrganizationByIdDbQuery request, CancellationToken cancellationToken) + { + BonesOrganization? organization = await dbContext.Organizations.FirstOrDefaultAsync(x => x.Id == request.OrganizationId, cancellationToken); + + if (organization is null) + { + return QueryResponse.Fail("Organization not found"); + } + + return organization; + } +} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbHandler.cs b/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbHandler.cs deleted file mode 100644 index 7663229..0000000 --- a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bones.Database.DbSets.OrganizationManagement; - -namespace Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; - -internal sealed class GetOrganizationByIdDbHandler(BonesDbContext dbContext) : IRequestHandler> -{ - public async Task> Handle(GetOrganizationByIdDbQuery request, CancellationToken cancellationToken) - { - BonesOrganization? organization = await dbContext.Organizations.FirstOrDefaultAsync(x => x.Id == request.OrganizationId, cancellationToken); - - if (organization is null) - { - return QueryResponse.Fail("Organization not found"); - } - - return organization; - } -} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQuery.cs b/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQuery.cs deleted file mode 100644 index b459fc3..0000000 --- a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Bones.Database.DbSets.OrganizationManagement; - -namespace Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; - -/// -/// DB Query to get the specified organization. -/// -/// -public sealed record GetOrganizationByIdDbQuery(Guid OrganizationId) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQueryValidator.cs b/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQueryValidator.cs deleted file mode 100644 index e2d43f4..0000000 --- a/Core/Bones.Database/Operations/OrganizationManagement/GetOrganizationByIdDb/GetOrganizationByIdDbQueryValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; - -internal sealed class GetOrganizationByIdDbQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.OrganizationId).NotNull().NotEqual(Guid.Empty); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativeByIdDb.cs b/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativeByIdDb.cs new file mode 100644 index 0000000..2f7469b --- /dev/null +++ b/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativeByIdDb.cs @@ -0,0 +1,26 @@ +using System; +using Bones.Database.DbSets.ProjectManagement; + +namespace Bones.Database.Operations.ProjectManagement.Initiatives; + +/// +/// DB Query for getting an initiative +/// +/// Internal ID of the initiative +public record GetInitiativesByIdDbQuery(Guid InitiativeId) : IRequest>; + +internal sealed class GetInitiativesByIdQueryDbValidator : AbstractValidator +{ + +} + +internal sealed class GetInitiativesByIdDbHandler(BonesDbContext dbContext) : IRequestHandler> +{ + public async Task> Handle(GetInitiativesByIdDbQuery request, CancellationToken cancellationToken) + { + return await dbContext.Initiatives + .Include(i => i.Queues) + .Include(i => i.Project) + .FirstOrDefaultAsync(i => i.Id == request.InitiativeId, cancellationToken); + } +} \ No newline at end of file diff --git a/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativesByProjectDb.cs b/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativesByProjectDb.cs index 510be8b..eaf01c1 100644 --- a/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativesByProjectDb.cs +++ b/Core/Bones.Database/Operations/ProjectManagement/Initiatives/GetInitiativesByProjectDb.cs @@ -1,10 +1,4 @@ -using Bones.Database.DbSets.AccountManagement; -using Bones.Database.DbSets.OrganizationManagement; using Bones.Database.DbSets.ProjectManagement; -using Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; -using Bones.Database.Operations.ProjectManagement.Projects; -using Bones.Shared.Backend.Enums; -using Bones.Shared.Consts; namespace Bones.Database.Operations.ProjectManagement.Initiatives; @@ -23,6 +17,9 @@ internal sealed class GetInitiativesByProjectDbHandler(BonesDbContext dbContext) { public async Task>> Handle(GetInitiativesByProjectDbQuery request, CancellationToken cancellationToken) { - return await dbContext.Initiatives.Where(i => i.Project.Id == request.ProjectId).ToListAsync(cancellationToken); + return await dbContext.Initiatives + .Include(i => i.Queues) + .Where(i => i.Project.Id == request.ProjectId) + .ToListAsync(cancellationToken); } } \ No newline at end of file diff --git a/Core/Bones.Database/Operations/WorkItemManagement/WorkItemQueues/GetWorkItemQueuesByInitiativeDb.cs b/Core/Bones.Database/Operations/WorkItemManagement/WorkItemQueues/GetWorkItemQueuesByInitiativeDb.cs new file mode 100644 index 0000000..a834152 --- /dev/null +++ b/Core/Bones.Database/Operations/WorkItemManagement/WorkItemQueues/GetWorkItemQueuesByInitiativeDb.cs @@ -0,0 +1,24 @@ +using Bones.Database.DbSets.WorkItemManagement; + +namespace Bones.Database.Operations.WorkItemManagement.WorkItemQueues; + +/// +/// Backend query for getting work item queues that belong to an initiative. +/// +/// Internal ID of the initiative +public record GetWorkItemQueuesByInitiativeDbQuery(Guid InitiativeId) : IRequest>>; + +internal sealed class GetWorkItemQueuesByInitiativeDbQueryValidator : AbstractValidator +{ + +} + +internal sealed class GetWorkItemQueuesByInitiativeDbHandler(BonesDbContext dbContext) : IRequestHandler>> +{ + public async Task>> Handle(GetWorkItemQueuesByInitiativeDbQuery request, CancellationToken cancellationToken) + { + return (await dbContext.Initiatives.Include(i => i.Queues).FirstOrDefaultAsync(i => i.Id == request.InitiativeId, cancellationToken))?.Queues + // Should only really happen if the initiative doesn't exist, but that would have been checked in the Logic layer before here anyways + ?? []; + } +} diff --git a/Core/Bones.Logic/BonesBackendModule.cs b/Core/Bones.Logic/BonesBackendModule.cs index 66830c3..3a2740c 100644 --- a/Core/Bones.Logic/BonesBackendModule.cs +++ b/Core/Bones.Logic/BonesBackendModule.cs @@ -1,9 +1,6 @@ using Autofac; using Bones.Logic.Models; -using Bones.Shared.Backend.PipelineBehaviors; using Bones.Shared.Exceptions; -using MediatR.Extensions.Autofac.DependencyInjection; -using MediatR.Extensions.Autofac.DependencyInjection.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailHandler.cs b/Core/Bones.Logic/Features/Accounts/ConfirmEmail.cs similarity index 59% rename from Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailHandler.cs rename to Core/Bones.Logic/Features/Accounts/ConfirmEmail.cs index adc5bbe..6f87237 100644 --- a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailHandler.cs +++ b/Core/Bones.Logic/Features/Accounts/ConfirmEmail.cs @@ -1,9 +1,33 @@ using Bones.Database.DbSets.AccountManagement; -using Bones.Database.Operations.AccountManagement.SetEmailConfirmedDateTimeDb; +using Bones.Database.Operations.AccountManagement; using Bones.Shared.Extensions; using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Accounts.ConfirmEmail; +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend request for confirming a users email +/// +/// +/// +/// +public sealed record ConfirmEmailQuery(Guid UserId, string Code, string? ChangedEmail) : IRequest>; + +internal class ConfirmEmailQueryValidator : AbstractValidator +{ + public ConfirmEmailQueryValidator() + { + RuleFor(x => x.UserId).NotNull().NotEmpty(); + RuleFor(x => x.Code).NotNull().NotEmpty(); + RuleFor(x => x.ChangedEmail).Custom(async (email, ctx) => + { + if (email != null && !await email.IsValidEmailAsync()) + { + ctx.AddFailure("ChangedEmail", "Email is not valid"); + } + }); + } +} internal class ConfirmEmailHandler(UserManager userManager, ISender sender) : IRequestHandler> { @@ -41,7 +65,7 @@ public async Task> Handle(ConfirmEmailQuery reques if (result.Succeeded) { - // So when we update the email, we need to update the username. + // When we update the email, we need to update the username to match. result = await userManager.SetUserNameAsync(user, request.ChangedEmail); } } diff --git a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQuery.cs b/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQuery.cs deleted file mode 100644 index 51d3bc2..0000000 --- a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Bones.Logic.Features.Accounts.ConfirmEmail; - -/// -/// Backend request for confirming a users email -/// -/// -/// -/// -public sealed record ConfirmEmailQuery(Guid UserId, string Code, string? ChangedEmail) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQueryValidator.cs b/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQueryValidator.cs deleted file mode 100644 index 2bd3813..0000000 --- a/Core/Bones.Logic/Features/Accounts/ConfirmEmail/ConfirmEmailQueryValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Accounts.ConfirmEmail; - -internal class ConfirmEmailQueryValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalHandler.cs b/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal.cs similarity index 50% rename from Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalHandler.cs rename to Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal.cs index 370187e..cc8bd10 100644 --- a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalHandler.cs +++ b/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal.cs @@ -1,7 +1,22 @@ +using System.Security.Claims; using Bones.Database.DbSets.AccountManagement; using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Accounts.GetUserByClaimsPrincipal; +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend request for getting a by a . +/// +/// +public sealed record GetUserByClaimsPrincipalQuery(ClaimsPrincipal? ClaimsPrincipal) : IRequest>; + +internal sealed class GetUserByClaimsPrincipalQueryValidator : AbstractValidator +{ + public GetUserByClaimsPrincipalQueryValidator() + { + RuleFor(x => x.ClaimsPrincipal).NotNull(); + } +} internal sealed class GetUserByClaimsPrincipalHandler(UserManager userManager) : IRequestHandler> { diff --git a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQuery.cs b/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQuery.cs deleted file mode 100644 index 8886538..0000000 --- a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQuery.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Security.Claims; -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Accounts.GetUserByClaimsPrincipal; - -/// -/// Backend request for getting a by a . -/// -/// -public sealed record GetUserByClaimsPrincipalQuery(ClaimsPrincipal? ClaimsPrincipal) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQueryValidator.cs b/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQueryValidator.cs deleted file mode 100644 index 88e17b6..0000000 --- a/Core/Bones.Logic/Features/Accounts/GetUserByClaimsPrincipal/GetUserByClaimsPrincipalQueryValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation.Results; - -namespace Bones.Logic.Features.Accounts.GetUserByClaimsPrincipal; - -internal sealed class GetUserByClaimsPrincipalQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.ClaimsPrincipal).NotNull().WithMessage("Claims Principal cannot be null"); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailHandler.cs b/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail.cs similarity index 64% rename from Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailHandler.cs rename to Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail.cs index 8cf3bc7..c4e110a 100644 --- a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailHandler.cs +++ b/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail.cs @@ -6,7 +6,29 @@ using Bones.Shared.Extensions; using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Accounts.QueueConfirmationEmail; +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend command for queueing a confirmation email. +/// +/// +/// +/// +public sealed record QueueConfirmationEmailCommand(BonesUser User, string Email, bool IsChange = false) : IRequest; + +internal class QueueConfirmationEmailCommandValidator : AbstractValidator +{ + public QueueConfirmationEmailCommandValidator() + { + RuleFor(x => x.Email).NotNull().NotEmpty().EmailAddress().CustomAsync(async (email, ctx, cancel) => + { + if (!await email.IsValidEmailAsync(cancel)) + { + ctx.AddFailure(nameof(QueueConfirmationEmailCommand.Email), "Email is invalid"); + } + }); + } +} internal class QueueConfirmationEmailHandler(UserManager userManager, BackendConfiguration config, ISender sender) : IRequestHandler { diff --git a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommand.cs b/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommand.cs deleted file mode 100644 index 14f53d5..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Accounts.QueueConfirmationEmail; - -/// -/// Backend command for queueing a confirmation email. -/// -/// -/// -/// -public sealed record QueueConfirmationEmailCommand(BonesUser User, string Email, bool IsChange = false) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommandValidator.cs b/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommandValidator.cs deleted file mode 100644 index 33cc8a3..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueConfirmationEmail/QueueConfirmationEmailCommandValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bones.Shared.Extensions; -using FluentValidation.Results; - -namespace Bones.Logic.Features.Accounts.QueueConfirmationEmail; - -internal class QueueConfirmationEmailCommandValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.Email).NotNull().NotEmpty().EmailAddress().CustomAsync(async (email, ctx, cancel) => - { - if (!await email.IsValidEmailAsync(cancel)) - { - ctx.AddFailure(new ValidationFailure(nameof(QueueConfirmationEmailCommand.Email), "Email domain is invalid")); - } - }); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailHandler.cs b/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail.cs similarity index 66% rename from Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailHandler.cs rename to Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail.cs index ef31c34..2fc4b82 100644 --- a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailHandler.cs +++ b/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Bones.Database.DbSets.AccountManagement; using Bones.Database.Operations.SystemQueues.ForgotPassword.AddForgotPasswordEmailToQueueDb; using Bones.Logic.Models; @@ -6,7 +7,27 @@ using Bones.Shared.Extensions; using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Accounts.QueueForgotPasswordEmail; +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend request for queueing a password reset email. +/// +/// +public sealed record QueueForgotPasswordEmailCommand([Required] string Email) : IRequest; + +internal class QueueForgotPasswordEmailCommandValidator : AbstractValidator +{ + public QueueForgotPasswordEmailCommandValidator() + { + RuleFor(x => x.Email).NotNull().NotEmpty().EmailAddress().CustomAsync(async (email, ctx, cancel) => + { + if (!await email.IsValidEmailAsync(cancel)) + { + ctx.AddFailure(nameof(QueueForgotPasswordEmailCommand.Email), "Email domain is invalid"); + } + }); + } +} internal class QueueForgotPasswordEmailHandler(UserManager userManager, BackendConfiguration config, ISender sender) : IRequestHandler { diff --git a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommand.cs b/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommand.cs deleted file mode 100644 index 08355a8..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Bones.Logic.Features.Accounts.QueueForgotPasswordEmail; - -/// -/// Backend request for queueing a password reset email. -/// -/// -public sealed record QueueForgotPasswordEmailCommand([Required] string Email) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommandValidator.cs b/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommandValidator.cs deleted file mode 100644 index 53e4405..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueForgotPasswordEmail/QueueForgotPasswordEmailCommandValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bones.Shared.Extensions; -using FluentValidation.Results; - -namespace Bones.Logic.Features.Accounts.QueueForgotPasswordEmail; - -internal class QueueForgotPasswordEmailCommandValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.Email).NotNull().NotEmpty().EmailAddress().CustomAsync(async (email, ctx, cancel) => - { - if (!await email.IsValidEmailAsync(cancel)) - { - ctx.AddFailure(new ValidationFailure(nameof(QueueForgotPasswordEmailCommand.Email), "Email domain is invalid")); - } - }); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail.cs b/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail.cs new file mode 100644 index 0000000..a3fa6b1 --- /dev/null +++ b/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail.cs @@ -0,0 +1,40 @@ +using Bones.Database.DbSets.AccountManagement; +using Bones.Shared.Extensions; +using Microsoft.AspNetCore.Identity; + +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend request for resending the confirmation email. +/// +/// The email address to resent the confirmation request to. +public sealed record QueueResendConfirmationEmailCommand(string Email) : IRequest; + +internal class QueueResendConfirmationEmailCommandValidator : AbstractValidator +{ + public QueueResendConfirmationEmailCommandValidator() + { + RuleFor(x => x.Email).NotNull().NotEmpty().EmailAddress().CustomAsync(async (email, ctx, cancel) => + { + if (!await email.IsValidEmailAsync(cancel)) + { + ctx.AddFailure(nameof(QueueResendConfirmationEmailCommand.Email), "Email domain is invalid"); + } + }); + } +} + +internal class QueueResendConfirmationEmailHandler(UserManager userManager, ISender sender) : IRequestHandler +{ + public async Task Handle(QueueResendConfirmationEmailCommand request, CancellationToken cancellationToken) + { + if (await userManager.FindByEmailAsync(request.Email) is not { } user) + { + return CommandResponse.Fail(); + } + + await sender.Send(new QueueConfirmationEmailCommand(user, request.Email), cancellationToken); + + return CommandResponse.Pass(user.Id); + } +} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommand.cs b/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommand.cs deleted file mode 100644 index 71afa51..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bones.Logic.Features.Accounts.QueueResendConfirmationEmail; - -/// -/// Backend request for resending the confirmation email. -/// -/// The email address to resent the confirmation request to. -public sealed record QueueResendConfirmationEmailCommand(string Email) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommandValidator.cs b/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommandValidator.cs deleted file mode 100644 index 3029de0..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Accounts.QueueResendConfirmationEmail; - -internal class QueueResendConfirmationEmailCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailHandler.cs b/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailHandler.cs deleted file mode 100644 index d13c057..0000000 --- a/Core/Bones.Logic/Features/Accounts/QueueResendConfirmationEmail/QueueResendConfirmationEmailHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; -using Bones.Logic.Features.Accounts.QueueConfirmationEmail; -using Microsoft.AspNetCore.Identity; - -namespace Bones.Logic.Features.Accounts.QueueResendConfirmationEmail; - -internal class QueueResendConfirmationEmailHandler(UserManager userManager, ISender sender) : IRequestHandler -{ - public async Task Handle(QueueResendConfirmationEmailCommand request, CancellationToken cancellationToken) - { - if (await userManager.FindByEmailAsync(request.Email) is not { } user) - { - return CommandResponse.Fail(); - } - - await sender.Send(new QueueConfirmationEmailCommand(user, request.Email), cancellationToken); - - return CommandResponse.Pass(user.Id); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQueryValidator.cs b/Core/Bones.Logic/Features/Accounts/RegisterUser.cs similarity index 55% rename from Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQueryValidator.cs rename to Core/Bones.Logic/Features/Accounts/RegisterUser.cs index c7f3992..e6fba5c 100644 --- a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQueryValidator.cs +++ b/Core/Bones.Logic/Features/Accounts/RegisterUser.cs @@ -1,8 +1,18 @@ +using Bones.Database.DbSets.AccountManagement; using Bones.Shared; +using Bones.Shared.Exceptions; using Bones.Shared.Extensions; using FluentValidation.Results; +using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Accounts.RegisterUser; +namespace Bones.Logic.Features.Accounts; + +/// +/// Backend request for registering a new user. +/// +/// Their email address +/// Their desired password +public sealed record RegisterUserQuery(string Email, string Password) : IRequest>; internal sealed class RegisterUserQueryValidator : AbstractValidator { @@ -46,4 +56,34 @@ public override Task ValidateAsync(ValidationContext userManager, ISender sender) : IRequestHandler> +{ + public async Task> Handle(RegisterUserQuery request, CancellationToken cancellationToken) + { + if (!userManager.SupportsUserEmail) + { + throw new BonesException($"{nameof(RegisterUserHandler)} requires a user store with email support."); + } + + if (string.IsNullOrEmpty(request.Email) || !await request.Email.IsValidEmailAsync(cancellationToken)) + { + return IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(request.Email)); + } + + BonesUser user = new(); + + await userManager.SetUserNameAsync(user, request.Email); + await userManager.SetEmailAsync(user, request.Email); + + IdentityResult result = await userManager.CreateAsync(user, request.Password); + + if (result.Succeeded) + { + await sender.Send(new QueueConfirmationEmailCommand(user, request.Email), cancellationToken); + } + + return result; + } } \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserHandler.cs b/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserHandler.cs deleted file mode 100644 index 68a340d..0000000 --- a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; -using Bones.Logic.Features.Accounts.QueueConfirmationEmail; -using Bones.Shared.Exceptions; -using Bones.Shared.Extensions; -using Microsoft.AspNetCore.Identity; - -namespace Bones.Logic.Features.Accounts.RegisterUser; - -internal class RegisterUserHandler(UserManager userManager, ISender sender) : IRequestHandler> -{ - public async Task> Handle(RegisterUserQuery request, CancellationToken cancellationToken) - { - if (!userManager.SupportsUserEmail) - { - throw new BonesException($"{nameof(RegisterUserHandler)} requires a user store with email support."); - } - - if (string.IsNullOrEmpty(request.Email) || !await request.Email.IsValidEmailAsync(cancellationToken)) - { - return IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(request.Email)); - } - - BonesUser user = new(); - - await userManager.SetUserNameAsync(user, request.Email); - await userManager.SetEmailAsync(user, request.Email); - - IdentityResult result = await userManager.CreateAsync(user, request.Password); - - if (result.Succeeded) - { - await sender.Send(new QueueConfirmationEmailCommand(user, request.Email), cancellationToken); - } - - return result; - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQuery.cs b/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQuery.cs deleted file mode 100644 index a5cb589..0000000 --- a/Core/Bones.Logic/Features/Accounts/RegisterUser/RegisterUserQuery.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Bones.Logic.Features.Accounts.RegisterUser; - -/// -/// Backend request for registering a new user. -/// -/// Their email address -/// Their desired password -public sealed record RegisterUserQuery(string Email, string Password) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/ResetPassword.cs b/Core/Bones.Logic/Features/Accounts/ResetPassword.cs new file mode 100644 index 0000000..8a15296 --- /dev/null +++ b/Core/Bones.Logic/Features/Accounts/ResetPassword.cs @@ -0,0 +1,21 @@ +using System; + +namespace Bones.Logic.Features.Accounts; + +/// +/// Request to reset password +/// +public sealed record ResetPasswordCommand : IRequest; + +internal class ResetPasswordCommandValidator : AbstractValidator +{ + +} + +internal class ResetPasswordHandler : IRequestHandler +{ + public Task Handle(ResetPasswordCommand request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommand.cs b/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommand.cs deleted file mode 100644 index 986744e..0000000 --- a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Accounts.ResetPassword; - -/// -/// Request to reset password -/// -public sealed record ResetPasswordCommand : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommandValidator.cs b/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommandValidator.cs deleted file mode 100644 index d27c6ce..0000000 --- a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Accounts.ResetPassword; - -internal class ResetPasswordCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordHandler.cs b/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordHandler.cs deleted file mode 100644 index b90616f..0000000 --- a/Core/Bones.Logic/Features/Accounts/ResetPassword/ResetPasswordHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bones.Logic.Features.Accounts.ResetPassword; - -internal class ResetPasswordHandler : IRequestHandler -{ - public Task Handle(ResetPasswordCommand request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQueryValidator.cs b/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission.cs similarity index 52% rename from Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQueryValidator.cs rename to Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission.cs index 8083601..45fe88e 100644 --- a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQueryValidator.cs +++ b/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission.cs @@ -1,10 +1,18 @@ -using FluentValidation.Results; +using Bones.Database.DbSets.AccountManagement; -namespace Bones.Logic.Features.Organizations.UserHasOrganizationPermission; +namespace Bones.Logic.Features.Organizations; + +/// +/// Checks if the user has permission to do the specified action in the organization. +/// +/// +/// +/// +public sealed record UserHasOrganizationPermissionQuery(Guid OrganizationId, BonesUser User, string Claim) : IRequest>; internal sealed class UserHasOrganizationPermissionQueryValidator : AbstractValidator { - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) + public UserHasOrganizationPermissionQueryValidator() { RuleFor(x => x.OrganizationId).NotNull().NotEqual(Guid.Empty); RuleFor(x => x.User).NotNull(); @@ -15,7 +23,5 @@ internal sealed class UserHasOrganizationPermissionQueryValidator : AbstractVali ctx.AddFailure("Claim contains '|', this means you probably called GetOrganizationWideClaimType(). Don't do that, just pass in the claim name."); } }); - - return base.ValidateAsync(context, cancellation); } } \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionHandler.cs b/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionHandler.cs deleted file mode 100644 index 3c374c9..0000000 --- a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Security.Claims; -using Bones.Database.DbSets.AccountManagement; -using Bones.Database.DbSets.OrganizationManagement; -using Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; -using Bones.Shared.Consts; -using Microsoft.AspNetCore.Identity; - -namespace Bones.Logic.Features.Organizations.UserHasOrganizationPermission; - -internal sealed class UserHasOrganizationPermissionHandler(UserManager userManager, RoleManager roleManager, ISender sender) : IRequestHandler> -{ - public async Task> Handle(UserHasOrganizationPermissionQuery request, CancellationToken cancellationToken) - { - BonesOrganization? organization = await sender.Send(new GetOrganizationByIdDbQuery(request.OrganizationId), cancellationToken); - - if (organization is null) - { - return QueryResponse.Fail("Organization not found"); - } - - string adminClaim = BonesClaimTypes.Role.Organization.GetOrganizationWideClaimType(organization.Id, BonesClaimTypes.Role.Organization.ORGANIZATION_ADMINISTRATOR); - string neededClaim = BonesClaimTypes.Role.Organization.GetOrganizationWideClaimType(organization.Id, request.Claim); - - foreach (string roleName in await userManager.GetRolesAsync(request.User)) - { - BonesRole? role = await roleManager.FindByNameAsync(roleName); - if (role is null) - { - return QueryResponse.Fail("Role not found"); - } - - IList claims = await roleManager.GetClaimsAsync(role); - if (claims.Any(claim => (claim.Type == adminClaim || claim.Type == neededClaim) && claim.Value == ClaimValues.YES)) - { - return true; - } - } - - return false; - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQuery.cs b/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQuery.cs deleted file mode 100644 index 54114c9..0000000 --- a/Core/Bones.Logic/Features/Organizations/UserHasOrganizationPermission/UserHasOrganizationPermissionQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Organizations.UserHasOrganizationPermission; - -/// -/// Checks if the user has permission to do the specified action in the organization. -/// -/// -/// -/// -public sealed record UserHasOrganizationPermissionQuery(Guid OrganizationId, BonesUser User, string Claim) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/CreateInitiative.cs b/Core/Bones.Logic/Features/Projects/Initiatives/CreateInitiative.cs index 81d3722..753bf21 100644 --- a/Core/Bones.Logic/Features/Projects/Initiatives/CreateInitiative.cs +++ b/Core/Bones.Logic/Features/Projects/Initiatives/CreateInitiative.cs @@ -1,9 +1,9 @@ using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.OrganizationManagement; -using Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; +using Bones.Database.Operations.OrganizationManagement; using Bones.Database.Operations.ProjectManagement.Initiatives.CreateInitiativeDb; using Bones.Database.Operations.ProjectManagement.Projects; -using Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; +using Bones.Logic.Features.Projects.Projects; using Bones.Shared.Backend.Enums; using Bones.Shared.Consts; diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativeById.cs b/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativeById.cs index cf3566d..98fde0e 100644 --- a/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativeById.cs +++ b/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativeById.cs @@ -1,5 +1,7 @@ +using System.Data; using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.ProjectManagement; +using Bones.Database.Operations.ProjectManagement.Initiatives; namespace Bones.Logic.Features.Projects.Initiatives; @@ -10,15 +12,19 @@ namespace Bones.Logic.Features.Projects.Initiatives; /// public sealed record GetInitiativeByIdQuery(Guid InitiativeId, BonesUser RequestingUser) : IRequest>; -internal sealed class GetInitiativeByIdQueryValidator +internal sealed class GetInitiativeByIdQueryValidator : AbstractValidator { - + public GetInitiativeByIdQueryValidator() + { + RuleFor(x => x.InitiativeId).NotNull().NotEmpty(); + RuleFor(x => x.RequestingUser).NotNull(); + } } -internal sealed class GetInitiativeByIdHandler : IRequestHandler> +internal sealed class GetInitiativeByIdHandler(ISender sender) : IRequestHandler> { - public Task> Handle(GetInitiativeByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetInitiativeByIdQuery request, CancellationToken cancellationToken) { - throw new NotImplementedException(); + return await sender.Send(new GetInitiativesByIdDbQuery(request.InitiativeId), cancellationToken); } } \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativesByProject.cs b/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativesByProject.cs index fb16425..296e318 100644 --- a/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativesByProject.cs +++ b/Core/Bones.Logic/Features/Projects/Initiatives/GetInitiativesByProject.cs @@ -1,17 +1,13 @@ using Bones.Database.DbSets.AccountManagement; -using Bones.Database.DbSets.OrganizationManagement; using Bones.Database.DbSets.ProjectManagement; -using Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; using Bones.Database.Operations.ProjectManagement.Initiatives; -using Bones.Database.Operations.ProjectManagement.Projects; -using Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; -using Bones.Shared.Backend.Enums; +using Bones.Logic.Features.Projects.Projects; using Bones.Shared.Consts; namespace Bones.Logic.Features.Projects.Initiatives; /// -/// Backend Command for creating an Initiative. +/// Backend query for getting initiatives that belong to a project. /// /// Internal ID of the project /// The user requesting this @@ -19,7 +15,11 @@ public record GetInitiativesByProjectQuery(Guid ProjectId, BonesUser RequestingU internal sealed class GetInitiativesByProjectQueryValidator : AbstractValidator { - + public GetInitiativesByProjectQueryValidator() + { + RuleFor(x => x.ProjectId).NotNull().NotEmpty(); + RuleFor(x => x.RequestingUser).NotNull(); + } } internal sealed class GetInitiativesByProjectHandler(ISender sender) : IRequestHandler>> diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById.cs b/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById.cs new file mode 100644 index 0000000..95a9796 --- /dev/null +++ b/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById.cs @@ -0,0 +1,34 @@ +using Bones.Database.DbSets.AccountManagement; +using Bones.Database.Operations.ProjectManagement.Initiatives.QueueDeleteInitiativeByIdDb; +using Bones.Shared.Consts; + +namespace Bones.Logic.Features.Projects.Initiatives; + +/// +/// Queues the deletion of the initiative +/// +/// +/// +public sealed record QueueDeleteInitiativeByIdCommand(Guid InitiativeId, BonesUser RequestingUser) : IRequest; + +internal sealed class QueueDeleteInitiativeByIdCommandValidator : AbstractValidator +{ + +} + +internal sealed class QueueDeleteInitiativeByIdHandler(ISender sender) : IRequestHandler +{ + public async Task Handle(QueueDeleteInitiativeByIdCommand request, CancellationToken cancellationToken) + { + const string perm = BonesClaimTypes.Role.Initiative.DELETE_INITIATIVE; + bool? hasPermission = + await sender.Send(new UserHasInitiativePermissionQuery(request.InitiativeId, request.RequestingUser, perm), cancellationToken); + + if (hasPermission != true) + { + return CommandResponse.Forbid(); + } + + return await sender.Send(new QueueDeleteInitiativeByIdDbCommand(request.InitiativeId), cancellationToken); + } +} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommand.cs b/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommand.cs deleted file mode 100644 index 22aa5d6..0000000 --- a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Initiatives.QueueDeleteInitiativeById; - -/// -/// Queues the deletion of the initiative -/// -/// -/// -public sealed record QueueDeleteInitiativeByIdCommand(Guid InitiativeId, BonesUser RequestingUser) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommandValidator.cs b/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommandValidator.cs deleted file mode 100644 index 98906c1..0000000 --- a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Projects.Initiatives.QueueDeleteInitiativeById; - -internal sealed class QueueDeleteInitiativeByIdCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdHandler.cs b/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdHandler.cs deleted file mode 100644 index 1d8bb32..0000000 --- a/Core/Bones.Logic/Features/Projects/Initiatives/QueueDeleteInitiativeById/QueueDeleteInitiativeByIdHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Bones.Database.Operations.ProjectManagement.Initiatives.QueueDeleteInitiativeByIdDb; - -namespace Bones.Logic.Features.Projects.Initiatives.QueueDeleteInitiativeById; - -internal sealed class QueueDeleteInitiativeByIdHandler(ISender sender) : IRequestHandler -{ - public async Task Handle(QueueDeleteInitiativeByIdCommand request, CancellationToken cancellationToken) - { - // TODO: Validate user permission here - return await sender.Send(new QueueDeleteInitiativeByIdDbCommand(request.InitiativeId), cancellationToken); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Initiatives/UserHasInitiativePermission.cs b/Core/Bones.Logic/Features/Projects/Initiatives/UserHasInitiativePermission.cs new file mode 100644 index 0000000..3962f2d --- /dev/null +++ b/Core/Bones.Logic/Features/Projects/Initiatives/UserHasInitiativePermission.cs @@ -0,0 +1,75 @@ + +using System.Security.Claims; +using Bones.Database.DbSets.AccountManagement; +using Bones.Database.DbSets.ProjectManagement; +using Bones.Database.Operations.ProjectManagement.Initiatives; +using Bones.Logic.Features.Projects.Projects; +using Bones.Shared.Consts; +using Microsoft.AspNetCore.Identity; + +namespace Bones.Logic.Features.Projects.Initiatives; + +/// +/// Checks if the user has permission to do the specified action in the initiative. +/// +/// +/// +/// +public sealed record UserHasInitiativePermissionQuery(Guid InitiativeId, BonesUser User, string Claim) : IRequest>; + +internal sealed class UserHasInitiativePermissionQueryValidator : AbstractValidator +{ + public UserHasInitiativePermissionQueryValidator() + { + RuleFor(x => x.InitiativeId).NotNull().NotEqual(Guid.Empty); + RuleFor(x => x.User).NotNull(); + RuleFor(x => x.Claim).NotNull().NotEmpty().Custom((claim, ctx) => + { + if (claim.Contains('|')) + { + ctx.AddFailure("Claim contains '|', this means you probably called GetInitiativeClaimType(). Don't do that, just pass in the claim name."); + } + }); + } +} + +internal sealed class UserHasInitiativePermissionHandler(UserManager userManager, RoleManager roleManager, ISender sender) : IRequestHandler> +{ + public async Task> Handle(UserHasInitiativePermissionQuery request, CancellationToken cancellationToken) + { + Initiative? initiative = await sender.Send(new GetInitiativesByIdDbQuery(request.InitiativeId), cancellationToken); + + if (initiative is null) + { + return QueryResponse.Fail("Initiative not found"); + } + + bool? projectPermission = await sender.Send( + new UserHasProjectPermissionQuery(initiative.Project.Id, request.User, request.Claim), + cancellationToken); + + if (projectPermission == true) + { + return true; + } + + foreach (string roleName in await userManager.GetRolesAsync(request.User)) + { + BonesRole? role = await roleManager.FindByNameAsync(roleName); + if (role is null) + { + return QueryResponse.Fail("Role not found"); + } + + string neededClaim = BonesClaimTypes.Role.Initiative.GetInitiativeClaimType(initiative.Id, request.Claim); + + IList claims = await roleManager.GetClaimsAsync(role); + if (claims.Any(claim => claim.Type == neededClaim && claim.Value == ClaimValues.YES)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectHandler.cs b/Core/Bones.Logic/Features/Projects/Projects/CreateProject.cs similarity index 65% rename from Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectHandler.cs rename to Core/Bones.Logic/Features/Projects/Projects/CreateProject.cs index 636f480..682a8a2 100644 --- a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectHandler.cs +++ b/Core/Bones.Logic/Features/Projects/Projects/CreateProject.cs @@ -1,10 +1,24 @@ +using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.OrganizationManagement; -using Bones.Database.Operations.OrganizationManagement.GetOrganizationByIdDb; +using Bones.Database.Operations.OrganizationManagement; using Bones.Database.Operations.ProjectManagement.Projects.CreateProjectDb; -using Bones.Logic.Features.Organizations.UserHasOrganizationPermission; +using Bones.Logic.Features.Organizations; using Bones.Shared.Consts; -namespace Bones.Logic.Features.Projects.Projects.CreateProject; +namespace Bones.Logic.Features.Projects.Projects; + +/// +/// DB Command for creating a Project. +/// +/// Name of the project +/// The user requesting this project be created +/// Optionally, the organization this project should belong to. +public record CreateProjectCommand(string Name, BonesUser RequestingUser, Guid? OrganizationId = null) : IRequest; + +internal sealed class CreateProjectCommandValidator : AbstractValidator +{ + +} internal sealed class CreateProjectHandler(ISender sender) : IRequestHandler { diff --git a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommand.cs b/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommand.cs deleted file mode 100644 index ca79fbd..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Projects.CreateProject; - -/// -/// DB Command for creating a Project. -/// -/// Name of the project -/// The user requesting this project be created -/// Optionally, the organization this project should belong to. -public record CreateProjectCommand(string Name, BonesUser RequestingUser, Guid? OrganizationId = null) : IRequest; diff --git a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommandValidator.cs b/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommandValidator.cs deleted file mode 100644 index a97512e..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/CreateProject/CreateProjectCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Projects.Projects.CreateProject; - -internal sealed class CreateProjectCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdHandler.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectById.cs similarity index 67% rename from Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdHandler.cs rename to Core/Bones.Logic/Features/Projects/Projects/GetProjectById.cs index d1e4ca7..4cb5d68 100644 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdHandler.cs +++ b/Core/Bones.Logic/Features/Projects/Projects/GetProjectById.cs @@ -1,8 +1,23 @@ +using Bones.Database.DbSets.AccountManagement; using Bones.Database.Operations.ProjectManagement.Projects; -using Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; using Bones.Shared.Consts; -namespace Bones.Logic.Features.Projects.Projects.GetProjectById; +namespace Bones.Logic.Features.Projects.Projects; + +/// +/// +/// +/// +/// +public sealed record GetProjectByIdQuery(Guid ProjectId, BonesUser RequestingUser) : IRequest>; + +internal sealed class GetProjectByIdQueryValidator : AbstractValidator +{ + public GetProjectByIdQueryValidator() + { + RuleFor(x => x.RequestingUser).NotNull(); + } +} internal sealed class GetProjectByIdHandler(ISender sender) : IRequestHandler> { diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQuery.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQuery.cs deleted file mode 100644 index ad761b5..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQuery.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectById; - -/// -/// -/// -/// -/// -public sealed record GetProjectByIdQuery(Guid ProjectId, BonesUser RequestingUser) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQueryValidator.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQueryValidator.cs deleted file mode 100644 index 3a7a7b9..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectById/GetProjectByIdQueryValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation.Results; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectById; - -internal sealed class GetProjectByIdQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.RequestingUser).NotNull(); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner.cs similarity index 64% rename from Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs rename to Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner.cs index 622023d..b75ff82 100644 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerHandler.cs +++ b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner.cs @@ -1,7 +1,26 @@ +using Bones.Database.DbSets.AccountManagement; using Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; using Bones.Shared.Backend.Enums; -namespace Bones.Logic.Features.Projects.Projects.GetProjectsByOwner; +namespace Bones.Logic.Features.Projects.Projects; + +/// +/// +/// +/// +/// +/// +public sealed record GetProjectsByOwnerQuery(OwnershipType OwnerType, Guid OwnerId, BonesUser RequestingUser) : IRequest>>; + +internal sealed class GetProjectsByOwnerQueryValidator : AbstractValidator +{ + public GetProjectsByOwnerQueryValidator() + { + RuleFor(x => x.OwnerType).NotNull().IsInEnum(); + RuleFor(x => x.OwnerId).NotNull().NotEqual(Guid.Empty); + RuleFor(x => x.RequestingUser).NotNull(); + } +} internal sealed class GetProjectsByOwnerHandler(ISender sender) : IRequestHandler>> { diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs deleted file mode 100644 index d6ac591..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; -using Bones.Shared.Backend.Enums; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectsByOwner; - -/// -/// -/// -/// -/// -/// -public sealed record GetProjectsByOwnerQuery(OwnershipType OwnerType, Guid OwnerId, BonesUser RequestingUser) : IRequest>>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQueryValidator.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQueryValidator.cs deleted file mode 100644 index 9af23fd..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsByOwner/GetProjectsByOwnerQueryValidator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FluentValidation.Results; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectsByOwner; - -internal sealed class GetProjectsByOwnerQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.OwnerType).NotNull().IsInEnum(); - RuleFor(x => x.OwnerId).NotNull().NotEqual(Guid.Empty); - RuleFor(x => x.RequestingUser).NotNull(); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess.cs similarity index 64% rename from Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs rename to Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess.cs index 6a0139a..eea56d1 100644 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessHandler.cs +++ b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess.cs @@ -1,7 +1,22 @@ +using Bones.Database.DbSets.AccountManagement; using Bones.Database.Operations.ProjectManagement.Projects.GetProjectsByOwnerDb; using Bones.Shared.Backend.Enums; -namespace Bones.Logic.Features.Projects.Projects.GetProjectsUserCanAccess; +namespace Bones.Logic.Features.Projects.Projects; + +/// +/// +/// +/// +public sealed record GetProjectsUserCanAccessQuery(BonesUser RequestingUser) : IRequest>>; + +internal sealed class GetProjectsUserCanAccessQueryValidator : AbstractValidator +{ + public GetProjectsUserCanAccessQueryValidator() + { + RuleFor(x => x.RequestingUser).NotNull(); + } +} internal sealed class GetProjectsUserCanAccessHandler(ISender sender) : IRequestHandler>> { @@ -20,4 +35,4 @@ public async Task>> Handle(GetProjectsUse return projectsUserCanAccess; } -} \ No newline at end of file +} diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs deleted file mode 100644 index 5e36df5..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectsUserCanAccess; - -/// -/// -/// -/// -public sealed record GetProjectsUserCanAccessQuery(BonesUser RequestingUser) : IRequest>>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs b/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs deleted file mode 100644 index 197704f..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/GetProjectsUserCanAccess/GetProjectsUserCanAccessQueryValidator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentValidation.Results; - -namespace Bones.Logic.Features.Projects.Projects.GetProjectsUserCanAccess; - -internal sealed class GetProjectsUserCanAccessQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.RequestingUser).NotNull(); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs b/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission.cs similarity index 65% rename from Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs rename to Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission.cs index 9ef682d..0f7adaa 100644 --- a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionHandler.cs +++ b/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission.cs @@ -2,12 +2,36 @@ using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.ProjectManagement; using Bones.Database.Operations.ProjectManagement.Projects; -using Bones.Logic.Features.Organizations.UserHasOrganizationPermission; +using Bones.Logic.Features.Organizations; using Bones.Shared.Backend.Enums; using Bones.Shared.Consts; using Microsoft.AspNetCore.Identity; -namespace Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; +namespace Bones.Logic.Features.Projects.Projects; + +/// +/// Checks if the user has permission to do the specified action in the project. +/// +/// +/// +/// +public sealed record UserHasProjectPermissionQuery(Guid ProjectId, BonesUser User, string Claim) : IRequest>; + +internal sealed class UserHasProjectPermissionQueryValidator : AbstractValidator +{ + public UserHasProjectPermissionQueryValidator() + { + RuleFor(x => x.ProjectId).NotNull().NotEqual(Guid.Empty); + RuleFor(x => x.User).NotNull(); + RuleFor(x => x.Claim).NotNull().NotEmpty().Custom((claim, ctx) => + { + if (claim.Contains('|')) + { + ctx.AddFailure("Claim contains '|', this means you probably called GetProjectClaimType(). Don't do that, just pass in the claim name."); + } + }); + } +} internal sealed class UserHasProjectPermissionHandler(UserManager userManager, RoleManager roleManager, ISender sender) : IRequestHandler> { diff --git a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQuery.cs b/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQuery.cs deleted file mode 100644 index 5060441..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQuery.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; - -/// -/// Checks if the user has permission to do the specified action in the project. -/// -/// -/// -/// -public sealed record UserHasProjectPermissionQuery(Guid ProjectId, BonesUser User, string Claim) : IRequest>; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQueryValidator.cs b/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQueryValidator.cs deleted file mode 100644 index b14b51d..0000000 --- a/Core/Bones.Logic/Features/Projects/Projects/UserHasProjectPermission/UserHasProjectPermissionQueryValidator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentValidation.Results; - -namespace Bones.Logic.Features.Projects.Projects.UserHasProjectPermission; - -internal sealed class UserHasProjectPermissionQueryValidator : AbstractValidator -{ - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new()) - { - RuleFor(x => x.ProjectId).NotNull().NotEqual(Guid.Empty); - RuleFor(x => x.User).NotNull(); - RuleFor(x => x.Claim).NotNull().NotEmpty().Custom((claim, ctx) => - { - if (claim.Contains('|')) - { - ctx.AddFailure("Claim contains '|', this means you probably called GetProjectClaimType(). Don't do that, just pass in the claim name."); - } - }); - - return base.ValidateAsync(context, cancellation); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommand.cs b/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommand.cs deleted file mode 100644 index 7c662d5..0000000 --- a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bones.Database.DbSets.AccountManagement; - -namespace Bones.Logic.Features.Projects.Queues.CreateQueue; - -/// -/// Command for creating a Queue. -/// -/// Name of the queue -/// Internal ID of the initiative -/// -public sealed record CreateQueueCommand(string Name, Guid InitiativeId, BonesUser RequestingUser) : IRequest; \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommandValidator.cs b/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommandValidator.cs deleted file mode 100644 index 74c7cae..0000000 --- a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueCommandValidator.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bones.Logic.Features.Projects.Queues.CreateQueue; - -internal sealed class CreateQueueCommandValidator : AbstractValidator -{ - -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueHandler.cs b/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueHandler.cs deleted file mode 100644 index 9897194..0000000 --- a/Core/Bones.Logic/Features/Projects/Queues/CreateQueue/CreateQueueHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Bones.Database.Operations.WorkItemManagement.WorkItemQueues.CreateQueueDb; - -namespace Bones.Logic.Features.Projects.Queues.CreateQueue; - -internal sealed class CreateQueueHandler(ISender sender) : IRequestHandler -{ - public async Task Handle(CreateQueueCommand request, CancellationToken cancellationToken) - { - // TODO: Check permission - return await sender.Send(new CreateQueueDbCommand(request.Name, request.InitiativeId), cancellationToken); - } -} \ No newline at end of file diff --git a/Core/Bones.Logic/Features/Projects/WorkItems/CreateWorkItemQueue.cs b/Core/Bones.Logic/Features/Projects/WorkItems/CreateWorkItemQueue.cs new file mode 100644 index 0000000..0f49ae7 --- /dev/null +++ b/Core/Bones.Logic/Features/Projects/WorkItems/CreateWorkItemQueue.cs @@ -0,0 +1,26 @@ +using Bones.Database.DbSets.AccountManagement; +using Bones.Database.Operations.WorkItemManagement.WorkItemQueues.CreateQueueDb; + +namespace Bones.Logic.Features.Projects.WorkItems; + +/// +/// Command for creating a Queue. +/// +/// Name of the queue +/// Internal ID of the initiative +/// +public sealed record CreateQueueCommand(string Name, Guid InitiativeId, BonesUser RequestingUser) : IRequest; + +internal sealed class CreateQueueCommandValidator : AbstractValidator +{ + +} + +internal sealed class CreateQueueHandler(ISender sender) : IRequestHandler +{ + public async Task Handle(CreateQueueCommand request, CancellationToken cancellationToken) + { + // TODO: Check permission + return await sender.Send(new CreateQueueDbCommand(request.Name, request.InitiativeId), cancellationToken); + } +} diff --git a/Core/Bones.Logic/Features/Projects/WorkItems/GetWorkItemQueuesByInitiative.cs b/Core/Bones.Logic/Features/Projects/WorkItems/GetWorkItemQueuesByInitiative.cs new file mode 100644 index 0000000..1aa1b9c --- /dev/null +++ b/Core/Bones.Logic/Features/Projects/WorkItems/GetWorkItemQueuesByInitiative.cs @@ -0,0 +1,37 @@ +using Bones.Database.DbSets.AccountManagement; +using Bones.Database.DbSets.WorkItemManagement; +using Bones.Database.Operations.WorkItemManagement.WorkItemQueues; +using Bones.Logic.Features.Projects.Initiatives; +using Bones.Logic.Features.Projects.Projects; +using Bones.Shared.Consts; + +namespace Bones.Logic.Features.Projects.WorkItems; + +/// +/// Backend query for getting work item queues that belong to an initiative. +/// +/// Internal ID of the initiative +/// The user requesting this +public record GetWorkItemQueuesByInitiativeQuery(Guid InitiativeId, BonesUser RequestingUser) : IRequest>>; + +internal sealed class GetWorkItemQueuesByInitiativeQueryValidator : AbstractValidator +{ + +} + +internal sealed class GetWorkItemQueuesByInitiativeHandler(ISender sender) : IRequestHandler>> +{ + public async Task>> Handle(GetWorkItemQueuesByInitiativeQuery request, CancellationToken cancellationToken) + { + const string perm = BonesClaimTypes.Role.WorkItemQueue.VIEW_QUEUE; + bool? hasOrganizationPermission = + await sender.Send(new UserHasInitiativePermissionQuery(request.InitiativeId, request.RequestingUser, perm), cancellationToken); + + if (hasOrganizationPermission != true) + { + return QueryResponse>.Forbid(); + } + + return await sender.Send(new GetWorkItemQueuesByInitiativeDbQuery(request.InitiativeId), cancellationToken); + } +} diff --git a/Core/Bones.Shared.Backend/Models/QueryResponse.cs b/Core/Bones.Shared.Backend/Models/QueryResponse.cs index 1747e07..4cc6860 100644 --- a/Core/Bones.Shared.Backend/Models/QueryResponse.cs +++ b/Core/Bones.Shared.Backend/Models/QueryResponse.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Identity; diff --git a/Core/Bones.Shared/Consts/BonesClaimTypes.cs b/Core/Bones.Shared/Consts/BonesClaimTypes.cs index 27da152..e48906c 100644 --- a/Core/Bones.Shared/Consts/BonesClaimTypes.cs +++ b/Core/Bones.Shared/Consts/BonesClaimTypes.cs @@ -146,34 +146,34 @@ public static string GetInitiativeClaimType(Guid initiativeId, string permission } /// - /// Queue level claim types for organization roles + /// WorkItemQueue level claim types for organization roles /// - public static class Queue + public static class WorkItemQueue { /// /// Claim type for if a user can view a queue /// - public const string VIEW_QUEUE = "ViewQueue"; + public const string VIEW_QUEUE = "ViewWorkItemQueue"; /// /// Claim type for if a user can create a queue /// - public const string CREATE_QUEUE = "CreateQueue"; + public const string CREATE_QUEUE = "CreateWorkItemQueue"; /// /// Claim type for if a user can delete a queue /// - public const string DELETE_QUEUE = "DeleteQueue"; + public const string DELETE_QUEUE = "DeleteWorkItemQueue"; /// /// Claim type for if a user can view the queues settings /// - public const string VIEW_QUEUE_SETTINGS = "ViewQueueSettings"; + public const string VIEW_QUEUE_SETTINGS = "ViewWorkItemQueueSettings"; /// /// Claim type for if a user can edit the queues settings /// - public const string EDIT_QUEUE_SETTINGS = "EditQueueSettings"; + public const string EDIT_QUEUE_SETTINGS = "EditWorkItemQueueSettings"; /// /// Gets the claim type for permissions that should apply to this queue diff --git a/Core/Bones.Shared/Consts/FrontEndUrls.cs b/Core/Bones.Shared/Consts/FrontEndUrls.cs index 77663a0..08986fe 100644 --- a/Core/Bones.Shared/Consts/FrontEndUrls.cs +++ b/Core/Bones.Shared/Consts/FrontEndUrls.cs @@ -98,10 +98,29 @@ public static class Initiative /// Initiative dashboard page /// public const string INITIATIVE_DASHBOARD = $"{_initiativeWithId}/Dashboard"; + + /// + /// Page to create a work item queue within an initiative + /// + public const string INITIATIVE_CREATE_WORKITEM_QUEUE = $"{_initiativeWithId}/CreateWorkItemQueue"; } } + /// + /// Work item pages + /// + public static class WorkItem + { + private const string _workItem = "/WorkItem"; + private const string _workItemQueueWithId = $"{_workItem}/Q{{WorkItemQueueId:guid}}"; + + /// + /// Work item queue dashboard page + /// + public const string WORKITEM_QUEUE_DASHBOARD = $"{_workItemQueueWithId}/Dashboard"; + + } /// /// System Admin pages diff --git a/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs b/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs index 854c0e4..1294b80 100644 --- a/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs +++ b/Frontend/Bones.Api.Client/AutoGenBonesApiClient.cs @@ -1099,8 +1099,8 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() var urlBuilder_ = new System.Text.StringBuilder(); - // Operation Path: "Project/{projectId}/dashboard" - urlBuilder_.Append("Project/"); + // Operation Path: "Project/P{projectId}/dashboard" + urlBuilder_.Append("Project/P"); urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(projectId, System.Globalization.CultureInfo.InvariantCulture))); urlBuilder_.Append("/dashboard"); @@ -1196,6 +1196,130 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() } } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Gets a initiatives dashboard information + /// + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetInitiativeDashboardAsync(System.Guid projectId, System.Guid initiativeId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (projectId == null) + throw new System.ArgumentNullException("projectId"); + + if (initiativeId == null) + throw new System.ArgumentNullException("initiativeId"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "Project/P{projectId}/I{initiativeId}/dashboard" + urlBuilder_.Append("Project/P"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(projectId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/I"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(initiativeId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/dashboard"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 401) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Unauthorized", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 403) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Forbidden", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException("Internal Server Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 400) + { + var objectResponse_ = await ReadObjectResponseAsync>>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new ApiException>>("Bad Request", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// /// Creates a new project @@ -1343,8 +1467,8 @@ private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() var urlBuilder_ = new System.Text.StringBuilder(); - // Operation Path: "Project/{projectId}/initiative/create" - urlBuilder_.Append("Project/"); + // Operation Path: "Project/P{projectId}/initiative/create" + urlBuilder_.Append("Project/P"); urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(projectId, System.Globalization.CultureInfo.InvariantCulture))); urlBuilder_.Append("/initiative/create"); @@ -1708,6 +1832,51 @@ public partial record ErrorResponse } + /// + /// Response for the GetInitiativeDashboardAsync endpoint + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial record GetInitiativeDashboardResponse + { + /// + /// The initiative ID + /// + + [System.Text.Json.Serialization.JsonPropertyName("initiativeId")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.Guid InitiativeId { get; set; } + + /// + /// The name of the Initiative + /// + + [System.Text.Json.Serialization.JsonPropertyName("initiativeName")] + public string InitiativeName { get; set; } + + /// + /// The ID of the project the initiative is in + /// + + [System.Text.Json.Serialization.JsonPropertyName("projectId")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.Guid ProjectId { get; set; } + + /// + /// The number of initiatives in the project + /// + + [System.Text.Json.Serialization.JsonPropertyName("workItemQueueCount")] + public int WorkItemQueueCount { get; set; } + + /// + /// A list of the initiatives in the project + /// + + [System.Text.Json.Serialization.JsonPropertyName("workItemQueues")] + public System.Collections.Generic.List WorkItemQueues { get; set; } + + } + /// /// Response for the GetMyProfile endpoint /// @@ -1963,6 +2132,36 @@ public partial record UnauthorizedResult } + /// + /// Model for the work item queues to be listed in an initiative + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] + public partial record WorkItemQueueListModel + { + /// + /// The work item queue ID + /// + + [System.Text.Json.Serialization.JsonPropertyName("workItemQueueId")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.Guid WorkItemQueueId { get; set; } + + /// + /// The name of the work item queue + /// + + [System.Text.Json.Serialization.JsonPropertyName("workItemQueueName")] + public string WorkItemQueueName { get; set; } + + /// + /// The number of work items in the work item queue + /// + + [System.Text.Json.Serialization.JsonPropertyName("workItemCount")] + public int WorkItemCount { get; set; } + + } + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] diff --git a/Frontend/Bones.WebUI/Bones.WebUI.csproj b/Frontend/Bones.WebUI/Bones.WebUI.csproj index 25c26d3..1bf0f20 100644 --- a/Frontend/Bones.WebUI/Bones.WebUI.csproj +++ b/Frontend/Bones.WebUI/Bones.WebUI.csproj @@ -35,7 +35,7 @@ - + @@ -48,4 +48,8 @@ + + + + diff --git a/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs b/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs index 7b14744..6e16df8 100644 --- a/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Account/MyProfilePage.razor.cs @@ -38,13 +38,13 @@ protected override async Task OnInitializedAsync() { GetMyProfileResponse response = await ApiClient.GetMyProfileAsync(); - await CreateDateTime.SetText(response.CreateDateTime.LocalDateTime.ToString(CultureInfo.CurrentCulture) ?? string.Empty); + await CreateDateTime.SetText(response.CreateDateTime.LocalDateTime.ToString(CultureInfo.CurrentCulture)); - await Email.SetText(response.Email ?? string.Empty); - await EmailConfirmed.SetText(response.EmailConfirmed.ToString() ?? string.Empty); + await Email.SetText(response.Email); + await EmailConfirmed.SetText(response.EmailConfirmed.ToString()); await EmailConfirmedDateTime.SetText(response.EmailConfirmedDateTime?.LocalDateTime.ToString(CultureInfo.CurrentCulture) ?? string.Empty); - await DisplayName.SetText(response.DisplayName ?? string.Empty); + await DisplayName.SetText(response.DisplayName); await base.OnInitializedAsync(); } diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateInitiativePage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/CreateInitiativePage.razor.cs index 2cc1c71..5c7be5b 100644 --- a/Frontend/Bones.WebUI/Pages/Project/CreateInitiativePage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Project/CreateInitiativePage.razor.cs @@ -1,6 +1,4 @@ -using System.Net; using Bones.Api.Client; -using Bones.Shared; using Bones.Shared.Consts; using Microsoft.AspNetCore.Components; using MudBlazor; diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs index 1d3707d..8331e7a 100644 --- a/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Project/CreateProjectPage.razor.cs @@ -1,6 +1,4 @@ -using System.Net; using Bones.Api.Client; -using Bones.Shared; using Bones.Shared.Consts; using Microsoft.AspNetCore.Components; using MudBlazor; diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor b/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor new file mode 100644 index 0000000..6b6de8b --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor @@ -0,0 +1,30 @@ +@attribute [Route(FrontEndUrls.Project.Initiative.INITIATIVE_CREATE_WORKITEM_QUEUE)] +@inject ILogger Logger +@layout AuthenticatedLayout + +

Create Work Item Queue

+ + + + + + +
+ Create +
+
+
+
+ + + + @foreach (string error in ValidationErrors) + { + + @error + + } + +
\ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor.cs new file mode 100644 index 0000000..6b2b6d8 --- /dev/null +++ b/Frontend/Bones.WebUI/Pages/Project/CreateWorkItemQueueInInitiativePage.razor.cs @@ -0,0 +1,65 @@ +using Bones.Api.Client; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Bones.WebUI.Pages.Project; + +/// +/// Page to create a work item queue in an initiative +/// +public partial class CreateWorkItemQueueInInitiativePage : ComponentBase +{ + /// + /// The ID of the project + /// + [Parameter] + public Guid ProjectId { get; set; } + + /// + /// The ID of the initiative + /// + [Parameter] + public Guid InitiativeId { get; set; } + + /// + /// Did the request to the API result in an error? + /// + public bool ApiError { get; set; } = false; + + /// + /// Is the form valid? + /// + public bool FormValid { get; set; } + + /// + /// The issues with the users inputs + /// + public string[] ValidationErrors { get; set; } = []; + + private MudTextField WorkItemQueueName { get; set; } = new(); + + /// + /// Send the request to register to the API, if it errors tell the user what went wrong. + /// + public async Task SendCreateRequestAsync() + { + try + { + ApiError = false; + + await Task.CompletedTask; + + //Guid workItemQueueId = await ApiClient.CreateWorkItemQueueAsync(InitiativeId, new() + //{ + // Name = WorkItemQueueName.Text + //}); + + //NavManager.NavigateTo(FrontEndUrls.Project.Initiative.INITIATIVE_DASHBOARD.Replace("{ProjectId:guid}", InitiativeId.ToString()).Replace("{InitiativeId:guid}", initiativeId.ToString())); + } + catch (ApiException ex) + { + Logger.LogError(ex, "Error while creating the work item queue"); + ApiError = true; + } + } +} \ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor b/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor index dc33358..bbd8dd3 100644 --- a/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor +++ b/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor @@ -11,25 +11,25 @@
Queues - Create Initiative + Create Work Item Queue
- Number of initiatives: @QueueCount + Number of Work Item queues: @QueueCount - Initiative ID - Initiative Name - Queue Count - Go to Initiative + Work Item Queue ID + Work Item Queue Name + Work Item Count + Go to Work Item Queue - @context.InitiativeId - @context.InitiativeName - @context.QueueCount - Dashboard + @context.WorkItemQueueId + @context.WorkItemQueueName + @context.WorkItemCount + Dashboard \ No newline at end of file diff --git a/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor.cs index 6a17186..934a48b 100644 --- a/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Project/InitiativeDashboardPage.razor.cs @@ -21,24 +21,24 @@ public partial class InitiativeDashboardPage : ComponentBase public Guid InitiativeId { get; set; } /// - /// The name of the project, received from the API + /// The name of the initiative, received from the API /// public string? InitiativeName { get; set; } /// - /// The number of initiatives on the project, received from the API + /// The number of queues on the initiative, received from the API /// public int? QueueCount { get; set; } /// - /// + /// Is the queue list still loading from the API? /// public bool QueueListLoading { get; set; } = true; /// - /// + /// The list of queues on the initiative, received from the API /// - public List QueueList { get; set; } = []; + public List QueueList { get; set; } = []; /// /// @@ -46,11 +46,12 @@ public partial class InitiativeDashboardPage : ComponentBase protected override async Task OnInitializedAsync() { // TODO: Garbage data for now, do real later - GetProjectDashboardResponse dashboardResponse = await ApiClient.GetProjectDashboardAsync(ProjectId); - InitiativeName = dashboardResponse.ProjectName; + GetInitiativeDashboardResponse dashboardResponse = await ApiClient.GetInitiativeDashboardAsync(ProjectId, InitiativeId); + + InitiativeName = dashboardResponse.InitiativeName; - QueueCount = dashboardResponse.InitiativeCount; - QueueList = dashboardResponse.Initiatives; + QueueCount = dashboardResponse.WorkItemQueueCount; + QueueList = dashboardResponse.WorkItemQueues; QueueListLoading = false; await base.OnInitializedAsync(); diff --git a/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs index 77d9f90..9c9cce9 100644 --- a/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs +++ b/Frontend/Bones.WebUI/Pages/Project/ProjectDashboardPage.razor.cs @@ -48,6 +48,26 @@ public partial class ProjectDashboardPage : ComponentBase /// /// protected override async Task OnInitializedAsync() + { + await FetchFromAPI(); + + await base.OnInitializedAsync(); + } + + /// + /// + /// + /// + protected override async Task OnParametersSetAsync() + { + InitiativeListLoading = true; + InitiativeList = []; + await FetchFromAPI(); + + await base.OnParametersSetAsync(); + } + + private async Task FetchFromAPI() { GetProjectDashboardResponse dashboardResponse = await ApiClient.GetProjectDashboardAsync(ProjectId); ProjectName = dashboardResponse.ProjectName; @@ -57,7 +77,5 @@ protected override async Task OnInitializedAsync() InitiativeCount = dashboardResponse.InitiativeCount; InitiativeList = dashboardResponse.Initiatives; InitiativeListLoading = false; - - await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Services/Bones.Api/Controllers/AnonymousController.cs b/Services/Bones.Api/Controllers/AnonymousController.cs index aa6fcf1..78e3911 100644 --- a/Services/Bones.Api/Controllers/AnonymousController.cs +++ b/Services/Bones.Api/Controllers/AnonymousController.cs @@ -1,9 +1,6 @@ using System.ComponentModel.DataAnnotations; using Bones.Api.Models; -using Bones.Logic.Features.Accounts.ConfirmEmail; -using Bones.Logic.Features.Accounts.QueueForgotPasswordEmail; -using Bones.Logic.Features.Accounts.QueueResendConfirmationEmail; -using Bones.Logic.Features.Accounts.RegisterUser; +using Bones.Logic.Features.Accounts; using Bones.Shared.Backend.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; diff --git a/Services/Bones.Api/Controllers/BonesControllerBase.cs b/Services/Bones.Api/Controllers/BonesControllerBase.cs index 708eaf4..d539d27 100644 --- a/Services/Bones.Api/Controllers/BonesControllerBase.cs +++ b/Services/Bones.Api/Controllers/BonesControllerBase.cs @@ -1,6 +1,6 @@ using System.Net.Mime; using Bones.Api.Models; -using Bones.Logic.Features.Accounts.GetUserByClaimsPrincipal; +using Bones.Logic.Features.Accounts; using Bones.Database.DbSets.AccountManagement; using Bones.Shared.Exceptions; using Microsoft.AspNetCore.Authorization; diff --git a/Services/Bones.Api/Controllers/LoginController.cs b/Services/Bones.Api/Controllers/LoginController.cs index 858026d..200a9bc 100644 --- a/Services/Bones.Api/Controllers/LoginController.cs +++ b/Services/Bones.Api/Controllers/LoginController.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using Bones.Api.Models; using Bones.Database.DbSets.AccountManagement; using Microsoft.AspNetCore.Authorization; diff --git a/Services/Bones.Api/Controllers/ProjectController.Responses.cs b/Services/Bones.Api/Controllers/ProjectController.Responses.cs index 0671bdd..b456d99 100644 --- a/Services/Bones.Api/Controllers/ProjectController.Responses.cs +++ b/Services/Bones.Api/Controllers/ProjectController.Responses.cs @@ -91,4 +91,66 @@ public sealed record InitiativeListModel [JsonRequired] public required int QueueCount { get; init; } } + + /// + /// Response for the GetInitiativeDashboardAsync endpoint + /// + [JsonSerializable(typeof(GetInitiativeDashboardResponse))] + public record GetInitiativeDashboardResponse + { + /// + /// The initiative ID + /// + [JsonRequired] + public required Guid InitiativeId { get; init; } + + /// + /// The name of the Initiative + /// + [JsonRequired] + public required string InitiativeName { get; init; } + + /// + /// The ID of the project the initiative is in + /// + [JsonRequired] + public required Guid ProjectId { get; init; } + + /// + /// The number of initiatives in the project + /// + [JsonRequired] + public required int WorkItemQueueCount { get; init; } + + /// + /// A list of the initiatives in the project + /// + [JsonRequired] + public required List WorkItemQueues { get; init; } + } + + /// + /// Model for the work item queues to be listed in an initiative + /// + [JsonSerializable(typeof(WorkItemQueueListModel))] + public sealed record WorkItemQueueListModel + { + /// + /// The work item queue ID + /// + [JsonRequired] + public required Guid WorkItemQueueId { get; init; } + + /// + /// The name of the work item queue + /// + [JsonRequired] + public required string WorkItemQueueName { get; init; } + + /// + /// The number of work items in the work item queue + /// + [JsonRequired] + public required int WorkItemCount { get; init; } + } } \ No newline at end of file diff --git a/Services/Bones.Api/Controllers/ProjectController.cs b/Services/Bones.Api/Controllers/ProjectController.cs index 923825c..4a0b360 100644 --- a/Services/Bones.Api/Controllers/ProjectController.cs +++ b/Services/Bones.Api/Controllers/ProjectController.cs @@ -1,9 +1,6 @@ using Bones.Api.Models; using Bones.Logic.Features.Projects.Initiatives; -using Bones.Logic.Features.Projects.Projects.CreateProject; -using Bones.Logic.Features.Projects.Projects.GetProjectById; -using Bones.Logic.Features.Projects.Projects.GetProjectsByOwner; -using Bones.Logic.Features.Projects.Projects.GetProjectsUserCanAccess; +using Bones.Logic.Features.Projects.Projects; using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.ProjectManagement; using Bones.Shared.Backend.Enums; @@ -82,7 +79,7 @@ public async ValueTask>> GetPro ///
/// /// Ok with the results if successful, otherwise BadRequest with a message of what went wrong. - [HttpGet("{projectId:guid}/dashboard", Name = "GetProjectDashboardAsync")] + [HttpGet("P{projectId:guid}/dashboard", Name = "GetProjectDashboardAsync")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType>(StatusCodes.Status400BadRequest)] public async ValueTask> GetProjectDashboardAsync(Guid projectId) @@ -91,7 +88,7 @@ public async ValueTask> GetProjectDash QueryResponse projectResponse = await Sender.Send(new GetProjectByIdQuery(projectId, currentUser)); QueryResponse> initiativesResponse = await Sender.Send(new GetInitiativesByProjectQuery(projectId, currentUser)); - if (!projectResponse.Success || projectResponse.Result is null || !initiativesResponse.Success || initiativesResponse.Result is null) + if (!projectResponse.Success || projectResponse.Result is null) { return BadRequest(projectResponse.FailureReasons); } @@ -124,6 +121,45 @@ public async ValueTask> GetProjectDash return resp; } + + /// + /// Gets a initiatives dashboard information + /// + /// + /// + /// Ok with the results if successful, otherwise BadRequest with a message of what went wrong. + [HttpGet("P{projectId:guid}/I{initiativeId:guid}/dashboard", Name = "GetInitiativeDashboardAsync")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType>(StatusCodes.Status400BadRequest)] + public async ValueTask> GetInitiativeDashboardAsync(Guid projectId, Guid initiativeId) + { + BonesUser currentUser = await GetCurrentBonesUserAsync(); + QueryResponse initiativeResponse = await Sender.Send(new GetInitiativeByIdQuery(initiativeId, currentUser)); + + if (!initiativeResponse.Success || initiativeResponse.Result is null) + { + return BadRequest(initiativeResponse.FailureReasons); + } + + Initiative initiative = initiativeResponse.Result; + + GetInitiativeDashboardResponse resp = new() + { + InitiativeId = initiative.Id, + InitiativeName = initiative.Name, + ProjectId = initiative.Project.Id, + WorkItemQueueCount = initiative.Queues.Count, + WorkItemQueues = initiative.Queues.Select(i => + new WorkItemQueueListModel + { + WorkItemQueueId = i.Id, + WorkItemQueueName = i.Name, + WorkItemCount = i.WorkItems.Count + }).ToList() + }; + + return resp; + } #endregion #region POST @@ -152,7 +188,7 @@ public async ValueTask> CreateProjectAsync([FromBody] CreateP /// The ID of the project to create this in /// The request /// Created if created, otherwise BadRequest with a message of what went wrong. - [HttpPost("{projectId:guid}/initiative/create", Name = "CreateInitiativeAsync")] + [HttpPost("P{projectId:guid}/initiative/create", Name = "CreateInitiativeAsync")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async ValueTask> CreateInitiativeAsync(Guid projectId, [FromBody] CreateInitiativeRequest request) diff --git a/Services/Bones.Api/Controllers/SysAdminController.cs b/Services/Bones.Api/Controllers/SysAdminController.cs index 9236163..4ded026 100644 --- a/Services/Bones.Api/Controllers/SysAdminController.cs +++ b/Services/Bones.Api/Controllers/SysAdminController.cs @@ -1,4 +1,3 @@ -using Bones.Api.Models; using Bones.Shared.Consts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,7 +13,7 @@ public class SysAdminController(ISender sender) : BonesControllerBase(sender) /// /// Ping, pong! /// - /// Super secret passphrase that should absolutely never be shared under any circumstances. + /// A super secret passphrase that should absolutely never be shared under any circumstances. [HttpPost("ping", Name = "PingAsync")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult PingAsync() diff --git a/Services/Bones.Api/OpenApi/swagger.json b/Services/Bones.Api/OpenApi/swagger.json index 6d1c298..c34f837 100644 --- a/Services/Bones.Api/OpenApi/swagger.json +++ b/Services/Bones.Api/OpenApi/swagger.json @@ -621,7 +621,7 @@ } } }, - "/Project/{projectId}/dashboard": { + "/Project/P{projectId}/dashboard": { "get": { "tags": [ "Project" @@ -700,6 +700,95 @@ } } }, + "/Project/P{projectId}/I{initiativeId}/dashboard": { + "get": { + "tags": [ + "Project" + ], + "summary": "Gets a initiatives dashboard information", + "operationId": "GetInitiativeDashboardAsync", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "initiativeId", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedResult" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetInitiativeDashboardResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/Project/create": { "post": { "tags": [ @@ -772,7 +861,7 @@ } } }, - "/Project/{projectId}/initiative/create": { + "/Project/P{projectId}/initiative/create": { "post": { "tags": [ "Project" @@ -969,6 +1058,48 @@ "additionalProperties": false, "description": "The response body is empty, this is a workaround for the limitations of the API client." }, + "GetInitiativeDashboardResponse": { + "required": [ + "initiativeId", + "initiativeName", + "projectId", + "workItemQueueCount", + "workItemQueues" + ], + "type": "object", + "properties": { + "initiativeId": { + "type": "string", + "description": "The initiative ID", + "format": "uuid" + }, + "initiativeName": { + "type": "string", + "description": "The name of the Initiative", + "nullable": true + }, + "projectId": { + "type": "string", + "description": "The ID of the project the initiative is in", + "format": "uuid" + }, + "workItemQueueCount": { + "type": "integer", + "description": "The number of initiatives in the project", + "format": "int32" + }, + "workItemQueues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkItemQueueListModel" + }, + "description": "A list of the initiatives in the project", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Response for the GetInitiativeDashboardAsync endpoint" + }, "GetMyProfileResponse": { "required": [ "createDateTime", @@ -1193,6 +1324,33 @@ } }, "additionalProperties": false + }, + "WorkItemQueueListModel": { + "required": [ + "workItemCount", + "workItemQueueId", + "workItemQueueName" + ], + "type": "object", + "properties": { + "workItemQueueId": { + "type": "string", + "description": "The work item queue ID", + "format": "uuid" + }, + "workItemQueueName": { + "type": "string", + "description": "The name of the work item queue", + "nullable": true + }, + "workItemCount": { + "type": "integer", + "description": "The number of work items in the work item queue", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Model for the work item queues to be listed in an initiative" } } } diff --git a/Services/Bones.BackgroundService/Program.cs b/Services/Bones.BackgroundService/Program.cs index f56becd..3c10ded 100644 --- a/Services/Bones.BackgroundService/Program.cs +++ b/Services/Bones.BackgroundService/Program.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Autofac; using Autofac.Extensions.DependencyInjection; using Bones.Logic; @@ -7,8 +6,6 @@ using Bones.Database.DbSets.AccountManagement; using Bones.Database.Extensions; using Bones.Shared.Backend.Extensions; -using Bones.Shared.Backend.PipelineBehaviors; -using MediatR.Extensions.Autofac.DependencyInjection.Builder; namespace Bones.BackgroundService; diff --git a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueConfirmationEmailTests.cs b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueConfirmationEmailTests.cs index d8d37ae..f33e0c2 100644 --- a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueConfirmationEmailTests.cs +++ b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueConfirmationEmailTests.cs @@ -1,5 +1,4 @@ -using Bones.Logic.Features.Accounts.QueueConfirmationEmail; -using Bones.Logic.Features.Accounts.RegisterUser; +using Bones.Logic.Features.Accounts; using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.SystemQueues; using Bones.Shared.Backend.Models; diff --git a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueForgotPasswordEmailTests.cs b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueForgotPasswordEmailTests.cs index 170a9e1..f8b1d5d 100644 --- a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueForgotPasswordEmailTests.cs +++ b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/QueueForgotPasswordEmailTests.cs @@ -1,5 +1,4 @@ -using Bones.Logic.Features.Accounts.QueueForgotPasswordEmail; -using Bones.Logic.Features.Accounts.RegisterUser; +using Bones.Logic.Features.Accounts; using Bones.Database.DbSets.SystemQueues; using Bones.Shared.Backend.Models; using Bones.Testing.Shared.Backend; diff --git a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/RegisterUserTests.cs b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/RegisterUserTests.cs index 93fef86..a7f868e 100644 --- a/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/RegisterUserTests.cs +++ b/Tests/Backend/Bones.Logic.UnitTests/Features/AccountManagement/RegisterUserTests.cs @@ -1,4 +1,4 @@ -using Bones.Logic.Features.Accounts.RegisterUser; +using Bones.Logic.Features.Accounts; using Bones.Database.DbSets.AccountManagement; using Bones.Database.DbSets.SystemQueues; using Bones.Shared.Backend.Models;