diff --git a/src/ProjectManagementSystem.Api/Controllers/Admin/MembersController.cs b/src/ProjectManagementSystem.Api/Controllers/Admin/MembersController.cs new file mode 100644 index 0000000..8b72277 --- /dev/null +++ b/src/ProjectManagementSystem.Api/Controllers/Admin/MembersController.cs @@ -0,0 +1,42 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ProjectManagementSystem.Api.Exceptions; +using ProjectManagementSystem.Api.Extensions; +using ProjectManagementSystem.Api.Models.Admin.Members; +using ProjectManagementSystem.Domain.Admin.Members; + +namespace ProjectManagementSystem.Api.Controllers.Admin +{ + [Authorize(Roles = "Admin")] + [ApiController] + [ProducesResponseType(401)] + public sealed class MembersController : ControllerBase + { + /// + /// Create member + /// + /// + /// Input model + [HttpPost("admin/users/{id}/members")] + [ProducesResponseType(400)] + [ProducesResponseType(typeof(ProblemDetails), 409)] + public async Task Create( + CancellationToken cancellationToken, + [FromRoute] Guid id, + [FromBody] CreateMemberBinding binding, + [FromServices] IUserRepository userRepository) + { + var user = await userRepository.Get(id, cancellationToken); + + user.AddMember(new Member(binding.Id, id, binding.ProjectId, binding.RoleId)); + + await userRepository.Save(user); + + return CreatedAtRoute("GetMemberAdminRoute", new {id = binding.Id}, null); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Controllers/Admin/RolesController.cs b/src/ProjectManagementSystem.Api/Controllers/Admin/RolesController.cs new file mode 100644 index 0000000..bb57ccf --- /dev/null +++ b/src/ProjectManagementSystem.Api/Controllers/Admin/RolesController.cs @@ -0,0 +1,94 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ProjectManagementSystem.Api.Exceptions; +using ProjectManagementSystem.Api.Models.Admin.Roles; +using ProjectManagementSystem.Domain.Admin.Roles; +using ProjectManagementSystem.Queries.Admin.Roles; + +namespace ProjectManagementSystem.Api.Controllers.Admin +{ + [Authorize(Roles = "Admin")] + [ApiController] + [ProducesResponseType(401)] + public sealed class RolesController : ControllerBase + { + /// + /// Create project + /// + /// Input model + [HttpPost("admin/roles")] + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(typeof(ProblemDetails), 409)] + public async Task Create( + CancellationToken cancellationToken, + [FromBody] CreateRoleBinding binding, + [FromServices] IRoleRepository roleRepository, + [FromServices] IPermissionRepository permissionRepository) + { + var role = await roleRepository.Get(binding.Id, cancellationToken); + + if (role != null) + if (!role.Name.Equals(binding.Name)) + throw new ApiException(HttpStatusCode.Conflict, ErrorCode.RoleAlreadyExists, + "Role already exists with other parameters"); + + role = new Role(binding.Id, binding.Name); + + foreach (var permissionId in binding.Permissions) + { + var permission = await permissionRepository.Get(permissionId, cancellationToken); + + if (permission == null) + throw new ApiException(HttpStatusCode.NotFound, ErrorCode.PermissionNotFound, "Permission not found"); + + var rolePermission = new RolePermission(binding.Id, permission.Id); + + role.AddRolePermission(rolePermission); + } + + await roleRepository.Save(role); + + return CreatedAtRoute("GetRoleAdminRoute", new {id = role.Id}, null); + } + + /// + /// Find roles + /// + /// Input model + [HttpGet("admin/roles", Name = "FindRolesAdminRoute")] + [ProducesResponseType(typeof(RoleListItemView), 200)] + public async Task Find( + CancellationToken cancellationToken, + [FromQuery] FindRolesBinding binding, + [FromServices] IMediator mediator) + { + return Ok(await mediator.Send(new RoleListQuery(binding.Offset, binding.Limit), cancellationToken)); + } + + /// + /// Get the role + /// + /// Role identifier + [HttpGet("admin/roles/{id}", Name = "GetRoleAdminRoute")] + [ProducesResponseType(typeof(RoleView), 200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + public async Task Get( + CancellationToken cancellationToken, + [FromRoute] Guid id, + [FromServices] IMediator mediator) + { + var role = await mediator.Send(new RoleQuery(id), cancellationToken); + + if (role == null) + throw new ApiException(HttpStatusCode.NotFound, ErrorCode.RoleNotFound, "Role not found"); + + return Ok(role); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Exceptions/ErrorCode.cs b/src/ProjectManagementSystem.Api/Exceptions/ErrorCode.cs index 9521683..02b168f 100644 --- a/src/ProjectManagementSystem.Api/Exceptions/ErrorCode.cs +++ b/src/ProjectManagementSystem.Api/Exceptions/ErrorCode.cs @@ -28,5 +28,8 @@ public sealed class ErrorCode public const string TimeEntryActivityAlreadyExists = "time_entry_activity_already_exists"; public const string TimeEntryNotFound = "time_entry_not_found"; public const string TimeEntryAlreadyExists = "time_entry_already_exists"; + public const string PermissionNotFound = "permission_not_found"; + public const string RoleNotFound = "role_not_found"; + public const string RoleAlreadyExists = "role_activity_already_exists"; } } \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateMemberBinding.cs b/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateMemberBinding.cs new file mode 100644 index 0000000..5286e6e --- /dev/null +++ b/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateMemberBinding.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using FluentValidation; + +namespace ProjectManagementSystem.Api.Models.Admin.Members +{ + public sealed class CreateMembersBinding + { + /// + /// + /// + public IEnumerable Members { get; set; } + } + + public sealed class CreateMembersBindingValidator : AbstractValidator + { + public CreateMembersBindingValidator() + { + RuleForEach(b => b.Members) + .NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateProjectRoleBinding.cs b/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateProjectRoleBinding.cs new file mode 100644 index 0000000..4d4fad5 --- /dev/null +++ b/src/ProjectManagementSystem.Api/Models/Admin/Members/CreateProjectRoleBinding.cs @@ -0,0 +1,36 @@ +using System; +using FluentValidation; + +namespace ProjectManagementSystem.Api.Models.Admin.Members +{ + public sealed class CreateMemberBinding + { + /// + /// Member identifier + /// + public Guid Id { get; set; } + + /// + /// Project identifier + /// + public Guid ProjectId { get; set; } + + /// + /// Role identifier + /// + public Guid RoleId { get; set; } + } + + public sealed class CreateMemberBindingValidator : AbstractValidator + { + public CreateMemberBindingValidator() + { + RuleFor(b => b.Id) + .NotEmpty(); + RuleFor(b => b.ProjectId) + .NotEmpty(); + RuleFor(b => b.RoleId) + .NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Models/Admin/Roles/CreateRoleBinding.cs b/src/ProjectManagementSystem.Api/Models/Admin/Roles/CreateRoleBinding.cs new file mode 100644 index 0000000..f1106ed --- /dev/null +++ b/src/ProjectManagementSystem.Api/Models/Admin/Roles/CreateRoleBinding.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using FluentValidation; + +namespace ProjectManagementSystem.Api.Models.Admin.Roles +{ + public sealed class CreateRoleBinding + { + /// + /// + /// + public Guid Id { get; set; } + + /// + /// + /// + public string Name { get; set; } + + /// + /// + /// + public IEnumerable Permissions { get; set; } + } + + public sealed class CreateRoleBindingValidator : AbstractValidator + { + public CreateRoleBindingValidator() + { + RuleFor(b => b.Id) + .NotEmpty(); + RuleFor(b => b.Name) + .NotEmpty(); + RuleForEach(b => b.Permissions) + .NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Models/Admin/Roles/FindProjectsBinding.cs b/src/ProjectManagementSystem.Api/Models/Admin/Roles/FindProjectsBinding.cs new file mode 100644 index 0000000..420bf43 --- /dev/null +++ b/src/ProjectManagementSystem.Api/Models/Admin/Roles/FindProjectsBinding.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using ProjectManagementSystem.Api.Models.Admin.Projects; + +namespace ProjectManagementSystem.Api.Models.Admin.Roles +{ + public sealed class FindRolesBinding + { + /// + /// Offset + /// + public int Offset { get; set; } = 0; + + /// + /// Limit + /// + public int Limit { get; set; } = 10; + } + + public sealed class FindRolesBindingValidator : AbstractValidator + { + public FindRolesBindingValidator() + { + RuleFor(b => b.Offset) + .GreaterThanOrEqualTo(0); + RuleFor(b => b.Limit) + .InclusiveBetween(2, 1000); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Api/Startup.cs b/src/ProjectManagementSystem.Api/Startup.cs index 30d82d0..8f8a2b0 100644 --- a/src/ProjectManagementSystem.Api/Startup.cs +++ b/src/ProjectManagementSystem.Api/Startup.cs @@ -41,6 +41,7 @@ public void ConfigureServices(IServiceCollection services) services .AddMvc(options => { + options.Filters.Add(typeof(PermissionAuthorizationHandler)); options.Filters.Add(typeof(ErrorHandlingFilter)); options.EnableEndpointRouting = false; }) diff --git a/src/ProjectManagementSystem.DatabaseMigrations/Entities/Member.cs b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Member.cs new file mode 100644 index 0000000..0d09b05 --- /dev/null +++ b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Member.cs @@ -0,0 +1,14 @@ +using System; + +namespace ProjectManagementSystem.DatabaseMigrations.Entities +{ + public sealed class Member + { + public Guid UserId { get; set; } + public User User { get; set; } + public Guid ProjectId { get; set; } + public Project Project { get; set; } + public Guid RoleId { get; set; } + public Role Role { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.DatabaseMigrations/Entities/Permission.cs b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Permission.cs new file mode 100644 index 0000000..107b50d --- /dev/null +++ b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Permission.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.DatabaseMigrations.Entities +{ + public sealed class Permission + { + public string PermissionId { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.DatabaseMigrations/Entities/Role.cs b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Role.cs new file mode 100644 index 0000000..4f38ce0 --- /dev/null +++ b/src/ProjectManagementSystem.DatabaseMigrations/Entities/Role.cs @@ -0,0 +1,10 @@ +using System; + +namespace ProjectManagementSystem.DatabaseMigrations.Entities +{ + public sealed class Role + { + public Guid RoleId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.DatabaseMigrations/Entities/RolePermission.cs b/src/ProjectManagementSystem.DatabaseMigrations/Entities/RolePermission.cs new file mode 100644 index 0000000..6aeaab9 --- /dev/null +++ b/src/ProjectManagementSystem.DatabaseMigrations/Entities/RolePermission.cs @@ -0,0 +1,12 @@ +using System; + +namespace ProjectManagementSystem.DatabaseMigrations.Entities +{ + public sealed class RolePermission + { + public Guid RoleId { get; set; } + public Role Role { get; set; } + public Guid PermissionId { get; set; } + public Permission Permission { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.DatabaseMigrations/ProjectManagementSystemDbContext.cs b/src/ProjectManagementSystem.DatabaseMigrations/ProjectManagementSystemDbContext.cs index c541e7b..ebe508f 100644 --- a/src/ProjectManagementSystem.DatabaseMigrations/ProjectManagementSystemDbContext.cs +++ b/src/ProjectManagementSystem.DatabaseMigrations/ProjectManagementSystemDbContext.cs @@ -45,7 +45,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsUnique(); builder.HasIndex(u => u.Email) .IsUnique(); - + builder.HasData(new User { UserId = new Guid("0ae12bbd-58ef-4c2e-87a6-2c2cb3f9592d"), @@ -60,7 +60,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ConcurrencyStamp = Guid.NewGuid() }); }); - + modelBuilder.Entity(builder => { builder.ToTable("RefreshToken"); @@ -72,7 +72,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(rt => rt.UserId) .IsRequired(); }); - + modelBuilder.Entity(builder => { builder.ToTable("IssuePriority"); @@ -84,7 +84,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(ip => ip.IsActive) .IsRequired(); }); - + modelBuilder.Entity(builder => { builder.ToTable("IssueStatus"); @@ -96,7 +96,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(@is => @is.IsActive) .IsRequired(); }); - + modelBuilder.Entity(builder => { builder.ToTable("Project"); @@ -116,7 +116,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(u => u.ConcurrencyStamp) .IsConcurrencyToken(); }); - + modelBuilder.Entity(builder => { builder.ToTable("Tracker"); @@ -128,11 +128,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(t => t.ConcurrencyStamp) .IsConcurrencyToken(); }); - + modelBuilder.Entity(builder => { builder.ToTable("ProjectTracker"); - builder.HasKey(pt => new { pt.ProjectId, pt.TrackerId }); + builder.HasKey(pt => new {pt.ProjectId, pt.TrackerId}); builder.HasOne(pt => pt.Project) .WithMany() .HasForeignKey(pt => pt.ProjectId) @@ -142,7 +142,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(pt => pt.TrackerId) .HasPrincipalKey(t => t.TrackerId); }); - + modelBuilder.Entity(builder => { builder.ToTable("Issue"); @@ -196,7 +196,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(i => i.AssigneeId) .HasPrincipalKey(p => p.UserId); }); - + modelBuilder.Entity(builder => { builder.ToTable("TimeEntryActivity"); @@ -210,7 +210,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.Property(tea => tea.ConcurrencyStamp) .IsConcurrencyToken(); }); - + modelBuilder.Entity(builder => { builder.ToTable("TimeEntry"); @@ -253,6 +253,56 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(te => te.ActivityId) .HasPrincipalKey(p => p.TimeEntryActivityId); }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Role"); + builder.HasKey(r => r.RoleId); + builder.Property(r => r.RoleId) + .ValueGeneratedNever(); + builder.Property(r => r.Name) + .IsRequired(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Permission"); + builder.HasKey(p => p.PermissionId); + builder.Property(p => p.PermissionId) + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("RolePermission"); + builder.HasKey(rp => new {rp.RoleId, rp.PermissionId}); + builder.HasOne(rp => rp.Role) + .WithMany() + .HasForeignKey(rp => rp.RoleId) + .HasPrincipalKey(r => r.RoleId); + builder.HasOne(rp => rp.Permission) + .WithMany() + .HasForeignKey(rp => rp.PermissionId) + .HasPrincipalKey(p => p.PermissionId); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Member"); + builder.HasKey(m => new {m.UserId, m.ProjectId, m.RoleId}); + builder.HasOne(m => m.User) + .WithMany() + .HasForeignKey(m => m.UserId) + .HasPrincipalKey(u => u.UserId); + builder.HasOne(m => m.Project) + .WithMany() + .HasForeignKey(m => m.ProjectId) + .HasPrincipalKey(p => p.ProjectId); + builder.HasOne(m => m.Role) + .WithMany() + .HasForeignKey(m => m.RoleId) + .HasPrincipalKey(r => r.RoleId); + }); } } } \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/IMemberRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Members/IMemberRepository.cs new file mode 100644 index 0000000..3200b3f --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/IMemberRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public interface IMemberRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/IProjectRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Members/IProjectRepository.cs new file mode 100644 index 0000000..076788a --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/IProjectRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public interface IProjectRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/IRoleRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Members/IRoleRepository.cs new file mode 100644 index 0000000..3758276 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/IRoleRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public interface IRoleRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/IUserRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Members/IUserRepository.cs new file mode 100644 index 0000000..ce46016 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/IUserRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public interface IUserRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + Task Save(User user); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/Member.cs b/src/ProjectManagementSystem.Domain/Admin/Members/Member.cs new file mode 100644 index 0000000..370a557 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/Member.cs @@ -0,0 +1,20 @@ +using System; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public sealed class Member + { + public Guid MemberId { get; } + public Guid UserId { get; } + public Guid ProjectId { get; } + public Guid RoleId { get; } + + public Member(Guid memberId, Guid userId, Guid projectId, Guid roleId) + { + MemberId = memberId; + UserId = userId; + ProjectId = projectId; + RoleId = roleId; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/Project.cs b/src/ProjectManagementSystem.Domain/Admin/Members/Project.cs new file mode 100644 index 0000000..a10fc8e --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/Project.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public sealed class Project + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/Role.cs b/src/ProjectManagementSystem.Domain/Admin/Members/Role.cs new file mode 100644 index 0000000..56078a8 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/Role.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public sealed class Role + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Members/User.cs b/src/ProjectManagementSystem.Domain/Admin/Members/User.cs new file mode 100644 index 0000000..1cd5f89 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Members/User.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace ProjectManagementSystem.Domain.Admin.Members +{ + public sealed class User + { + public Guid Id { get; } + private List _members = new List(); + public IEnumerable Members => _members; + private Guid _concurrencyStamp; + + public void AddMember(Member member) + { + _members.Add(member); + _concurrencyStamp = Guid.NewGuid(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/IPermissionRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/IPermissionRepository.cs new file mode 100644 index 0000000..d46a793 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/IPermissionRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public interface IPermissionRepository + { + Task Get(string id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/IRoleRepository.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/IRoleRepository.cs new file mode 100644 index 0000000..bb586ff --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/IRoleRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public interface IRoleRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + Task Get(string name, CancellationToken cancellationToken); + Task Save(Role role); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/Permission.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/Permission.cs new file mode 100644 index 0000000..b47b986 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/Permission.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public sealed class Permission + { + public string Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/Role.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/Role.cs new file mode 100644 index 0000000..6e7d765 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/Role.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public sealed class Role + { + public Guid Id { get; } + public string Name { get; private set; } + private List _rolePermissions = new List(); + public IEnumerable RolePermissions => _rolePermissions; + private Guid _concurrencyStamp; + + public Role(Guid id, string name) + { + Id = id; + Name = name; + _concurrencyStamp = Guid.NewGuid(); + } + + public void AddRolePermission(RolePermission rolePermission) + { + _rolePermissions.Add(rolePermission); + _concurrencyStamp = Guid.NewGuid(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/RoleCreationService.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/RoleCreationService.cs new file mode 100644 index 0000000..0ae6a8a --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/RoleCreationService.cs @@ -0,0 +1,7 @@ +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public sealed class RoleCreationService + { + + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/Admin/Roles/RolePermission.cs b/src/ProjectManagementSystem.Domain/Admin/Roles/RolePermission.cs new file mode 100644 index 0000000..4ea5e9a --- /dev/null +++ b/src/ProjectManagementSystem.Domain/Admin/Roles/RolePermission.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectManagementSystem.Domain.Admin.Roles +{ + public sealed class RolePermission + { + public Guid RoleId { get; } + public string PermissionId { get; } + + public RolePermission(Guid roleId, string permissionId) + { + RoleId = roleId; + PermissionId = permissionId; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/IMemberRepository.cs b/src/ProjectManagementSystem.Domain/User/Members/IMemberRepository.cs new file mode 100644 index 0000000..1007151 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/IMemberRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public interface IMemberRepository + { + Task Get(Guid userId, Guid projectId, Guid roleId, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/IPermissionRepository.cs b/src/ProjectManagementSystem.Domain/User/Members/IPermissionRepository.cs new file mode 100644 index 0000000..e72aebc --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/IPermissionRepository.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public interface IPermissionRepository + { + Task Get(string id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/IProjectRepository.cs b/src/ProjectManagementSystem.Domain/User/Members/IProjectRepository.cs new file mode 100644 index 0000000..d2f5987 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/IProjectRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public interface IProjectRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/IUserRepository.cs b/src/ProjectManagementSystem.Domain/User/Members/IUserRepository.cs new file mode 100644 index 0000000..189bc25 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/IUserRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public interface IUserRepository + { + Task Get(Guid id, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/Member.cs b/src/ProjectManagementSystem.Domain/User/Members/Member.cs new file mode 100644 index 0000000..6d98694 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/Member.cs @@ -0,0 +1,21 @@ +using System; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class Member + { + public Guid UserId { get; set; } + public User User { get; set; } + public Guid ProjectId { get; set; } + public Project Project { get; set; } + public Guid RoleId { get; set; } + public Role Role { get; set; } + + public Member(Guid userId, Guid projectId, Guid roleId) + { + UserId = userId; + ProjectId = projectId; + RoleId = roleId; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/Permission.cs b/src/ProjectManagementSystem.Domain/User/Members/Permission.cs new file mode 100644 index 0000000..2855b0c --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/Permission.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class Permission + { + public string Id { get; } + private List _rolePermissions = new List(); + public IEnumerable RolePermissions => _rolePermissions; + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/Project.cs b/src/ProjectManagementSystem.Domain/User/Members/Project.cs new file mode 100644 index 0000000..cbe96a9 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/Project.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class Project + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/Role.cs b/src/ProjectManagementSystem.Domain/User/Members/Role.cs new file mode 100644 index 0000000..9bc9126 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/Role.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class Role + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/RolePermission.cs b/src/ProjectManagementSystem.Domain/User/Members/RolePermission.cs new file mode 100644 index 0000000..65a8356 --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/RolePermission.cs @@ -0,0 +1,16 @@ +using System; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class RolePermission + { + public Guid RoleId { get; } + public string PermissionId { get; } + + public RolePermission(Guid roleId, string permissionId) + { + RoleId = roleId; + PermissionId = permissionId; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/User.cs b/src/ProjectManagementSystem.Domain/User/Members/User.cs new file mode 100644 index 0000000..5ee0bbb --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/User.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public sealed class User + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Domain/User/Members/UserAuthorizationService.cs b/src/ProjectManagementSystem.Domain/User/Members/UserAuthorizationService.cs new file mode 100644 index 0000000..a20ae4f --- /dev/null +++ b/src/ProjectManagementSystem.Domain/User/Members/UserAuthorizationService.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace ProjectManagementSystem.Domain.User.Members +{ + public class UserAuthorizationService + { + private readonly IUserRepository _userRepository; + private readonly IProjectRepository _projectRepository; + private readonly IPermissionRepository _permissionRepository; + private readonly IMemberRepository _memberRepository; + + public UserAuthorizationService(IUserRepository userRepository, IProjectRepository projectRepository, + IPermissionRepository permissionRepository, IMemberRepository memberRepository) + { + _userRepository = userRepository; + _projectRepository = projectRepository; + _permissionRepository = permissionRepository; + _memberRepository = memberRepository; + } + + public async Task Authorization(Guid userId, Guid projectId, string permissionName, + CancellationToken cancellationToken) + { + var user = await _userRepository.Get(userId, cancellationToken); + + if (user == null) + throw new Exception(); + + var project = await _projectRepository.Get(projectId, cancellationToken); + + if (project == null) + throw new Exception(); + + var permission = await _permissionRepository.Get(permissionName, cancellationToken); + + if (permission == null) + throw new Exception(); + + foreach (var rolePermission in permission.RolePermissions) + { + if (await _memberRepository.Get(userId, projectId, rolePermission.RoleId, cancellationToken) != null) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserDbContext.cs b/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserDbContext.cs new file mode 100644 index 0000000..ddc4e6b --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectManagementSystem.Infrastructure.Admin.Members +{ + public sealed class UserDbContext : DbContext + { + public UserDbContext(DbContextOptions options) : base(options) { } + + internal DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => + { + builder.ToTable("User"); + builder.HasKey(u => u.Id); + builder.Property(u => u.Id) + .HasColumnName("UserId") + .ValueGeneratedNever(); + builder.Property("_concurrencyStamp") + .HasColumnName("ConcurrencyStamp") + .IsConcurrencyToken(); + }); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserRepository.cs b/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserRepository.cs new file mode 100644 index 0000000..4844189 --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/Admin/Members/UserRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Domain.Admin.Members; + +namespace ProjectManagementSystem.Infrastructure.Admin.Members +{ + public sealed class UserRepository : IUserRepository + { + private readonly UserDbContext _context; + + public UserRepository(UserDbContext context) + { + _context = context; + } + + public async Task Get(Guid userId, CancellationToken cancellationToken) + { + return await _context.Users + .SingleOrDefaultAsync(u => u.Id == userId, cancellationToken); + } + + public async Task Save(Domain.Admin.Members.User user) + { + if (_context.Entry(user).State == EntityState.Detached) + await _context.Users.AddAsync(user); + + await _context.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/User/Members/MemberDbContext.cs b/src/ProjectManagementSystem.Infrastructure/User/Members/MemberDbContext.cs new file mode 100644 index 0000000..f07988b --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/User/Members/MemberDbContext.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectManagementSystem.Infrastructure.User.Members +{ + public sealed class MemberDbContext : DbContext + { + public MemberDbContext(DbContextOptions options) : base(options) { } + + internal DbSet Users { get; set; } + internal DbSet Projects { get; set; } + internal DbSet Permissions { get; set; } + internal DbSet Members { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => + { + builder.ToTable("User"); + builder.HasKey(u => u.Id); + builder.Property(u => u.Id) + .HasColumnName("UserId"); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Project"); + builder.HasKey(p => p.Id); + builder.Property(p => p.Id) + .HasColumnName("ProjectId"); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("RolePermission"); + builder.HasKey(rp => new {rp.RoleId, rp.PermissionId}); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Permission"); + builder.HasKey(p => p.Id); + builder.Property(p => p.Id) + .HasColumnName("PermissionId"); + builder.HasMany(p => p.RolePermissions) + .WithOne() + .HasForeignKey(rp => rp.PermissionId) + .HasPrincipalKey(p => p.Id); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Member"); + builder.HasOne(m => m.User) + .WithMany() + .HasForeignKey(m => m.UserId) + .HasPrincipalKey(u => u.Id); + builder.HasOne(m => m.Project) + .WithMany() + .HasForeignKey(m => m.ProjectId) + .HasPrincipalKey(p => p.Id); + builder.HasOne(m => m.Role) + .WithMany() + .HasForeignKey(m => m.RoleId) + .HasPrincipalKey(r => r.Id); + }); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/User/Members/MemberRepository.cs b/src/ProjectManagementSystem.Infrastructure/User/Members/MemberRepository.cs new file mode 100644 index 0000000..eda9759 --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/User/Members/MemberRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Domain.User.Members; + +namespace ProjectManagementSystem.Infrastructure.User.Members +{ + public sealed class MemberRepository : IMemberRepository + { + private readonly MemberDbContext _context; + + public MemberRepository(MemberDbContext context) + { + _context = context; + } + + public async Task Get(Guid userId, Guid projectId, Guid roleId, CancellationToken cancellationToken) + { + return await _context.Members.SingleOrDefaultAsync(m => + m.UserId == userId && + m.ProjectId == projectId && + m.RoleId == roleId, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/User/Members/PermissionRepository.cs b/src/ProjectManagementSystem.Infrastructure/User/Members/PermissionRepository.cs new file mode 100644 index 0000000..a1c5d1b --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/User/Members/PermissionRepository.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Domain.User.Members; + +namespace ProjectManagementSystem.Infrastructure.User.Members +{ + public sealed class PermissionRepository : IPermissionRepository + { + private readonly MemberDbContext _context; + + public PermissionRepository(MemberDbContext context) + { + _context = context; + } + + public async Task Get(string id, CancellationToken cancellationToken) + { + return await _context.Permissions.SingleOrDefaultAsync(p => p.Id == id, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/User/Members/ProjectRepository.cs b/src/ProjectManagementSystem.Infrastructure/User/Members/ProjectRepository.cs new file mode 100644 index 0000000..f5cbea0 --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/User/Members/ProjectRepository.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Domain.User.Members; + +namespace ProjectManagementSystem.Infrastructure.User.Members +{ + public sealed class ProjectRepository : IProjectRepository + { + private readonly MemberDbContext _context; + + public ProjectRepository(MemberDbContext context) + { + _context = context; + } + + public async Task Get(Guid id, CancellationToken cancellationToken) + { + return await _context.Projects.SingleOrDefaultAsync(u => u.Id == id, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Infrastructure/User/Members/UserRepository.cs b/src/ProjectManagementSystem.Infrastructure/User/Members/UserRepository.cs new file mode 100644 index 0000000..12dbd27 --- /dev/null +++ b/src/ProjectManagementSystem.Infrastructure/User/Members/UserRepository.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Domain.User.Members; + +namespace ProjectManagementSystem.Infrastructure.User.Members +{ + public sealed class UserRepository : IUserRepository + { + private readonly MemberDbContext _context; + + public UserRepository(MemberDbContext context) + { + _context = context; + } + + public async Task Get(Guid id, CancellationToken cancellationToken) + { + return await _context.Users.SingleOrDefaultAsync(u => u.Id == id, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Issue.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Issue.cs new file mode 100644 index 0000000..b749646 --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Issue.cs @@ -0,0 +1,28 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class Issue + { + public Guid Id { get; } + public long Number { get; } + public string Title { get; } + public string Description { get; } + public DateTime CreateDate { get; } + public DateTime? UpdateDate { get; } + public DateTime? StartDate { get; } + public DateTime? DueDate { get; } + public Guid ProjectId { get; } + public Project Project { get; } + public Guid TrackerId { get; } + public Tracker Tracker { get; } + public Guid StatusId { get; } + public IssueStatus Status { get; } + public Guid PriorityId { get; } + public IssuePriority Priority { get; } + public Guid AuthorId { get; } + public User Author { get; } + public Guid? AssigneeId { get; } + public User? Assignee { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueDbContext.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueDbContext.cs new file mode 100644 index 0000000..36afde0 --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueDbContext.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + public sealed class IssueDbContext : DbContext + { + public IssueDbContext(DbContextOptions options) : base(options) { } + + internal DbSet Issues { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => + { + builder.ToTable("Project"); + builder.HasKey(p => p.Id); + builder.Property(p => p.Id) + .HasColumnName("ProjectId"); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Tracker"); + builder.HasKey(t => t.Id); + builder.Property(t => t.Id) + .HasColumnName("TrackerId"); + builder.Property(t => t.Name); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("IssueStatus"); + builder.HasKey(@is => @is.Id); + builder.Property(@is => @is.Id) + .HasColumnName("IssueStatusId"); + builder.Property(@is => @is.Name); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("IssuePriority"); + builder.HasKey(ip => ip.Id); + builder.Property(ip => ip.Id) + .HasColumnName("IssuePriorityId"); + builder.Property(ip => ip.Name); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("User"); + builder.HasKey(u => u.Id); + builder.Property(u => u.Id) + .HasColumnName("UserId"); + builder.Property(u => u.Name); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Issue"); + builder.HasKey(i => i.Id); + builder.Property(i => i.Id) + .HasColumnName("IssueId"); + builder.Property(i => i.Number); + builder.Property(i => i.Title); + builder.Property(i => i.Description); + builder.Property(i => i.CreateDate); + builder.Property(i => i.StartDate); + builder.Property(i => i.DueDate); + builder.Property(i => i.TrackerId); + builder.Property(i => i.StatusId); + builder.Property(i => i.PriorityId); + builder.Property(i => i.AuthorId); + builder.Property(i => i.AssigneeId); + builder.HasOne(i => i.Project) + .WithMany() + .HasForeignKey(i => i.ProjectId) + .HasPrincipalKey(p => p.Id); + builder.HasOne(i => i.Tracker) + .WithMany() + .HasForeignKey(i => i.TrackerId) + .HasPrincipalKey(t => t.Id); + builder.HasOne(i => i.Status) + .WithMany() + .HasForeignKey(i => i.StatusId) + .HasPrincipalKey(@is => @is.Id); + builder.HasOne(i => i.Priority) + .WithMany() + .HasForeignKey(i => i.PriorityId) + .HasPrincipalKey(ip => ip.Id); + builder.HasOne(i => i.Author) + .WithMany() + .HasForeignKey(i => i.AuthorId) + .HasPrincipalKey(a => a.Id); + builder.HasOne(i => i.Assignee) + .WithMany() + .HasForeignKey(i => i.AssigneeId) + .HasPrincipalKey(p => p.Id); + }); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueListQueryHandler.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueListQueryHandler.cs new file mode 100644 index 0000000..4b982fc --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueListQueryHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Queries.User.Issues; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + public sealed class IssueListQueryHandler : IRequestHandler> + { + private readonly IssueDbContext _context; + + public IssueListQueryHandler(IssueDbContext context) + { + _context = context; + } + + public async Task> Handle(IssueListQuery query, CancellationToken cancellationToken) + { + var sql = _context.Issues.AsNoTracking() + .OrderBy(p => p.CreateDate) + .Select(i => new IssueListItemView + { + Id = i.Id, + Number = i.Number, + Title = i.Title, + TrackerName = i.Tracker.Name, + StatusName = i.Status.Name, + PriorityName = i.Priority.Name, + AssigneeName = i.Assignee.Name, + UpdateDate = i.UpdateDate ?? i.CreateDate, + }) + .AsQueryable(); + + return new Page + { + Limit = query.Limit, + Offset = query.Offset, + Total = await sql.CountAsync(cancellationToken), + Items = await sql.Skip(query.Offset).Take(query.Limit).ToListAsync(cancellationToken) + }; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssuePriority.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssuePriority.cs new file mode 100644 index 0000000..13cb65c --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssuePriority.cs @@ -0,0 +1,10 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class IssuePriority + { + public Guid Id { get; } + public string Name { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueQueryHandler.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueQueryHandler.cs new file mode 100644 index 0000000..a4cfc2c --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueQueryHandler.cs @@ -0,0 +1,49 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.EntityFrameworkCore; +using ProjectManagementSystem.Queries.User.Issues; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + public sealed class IssueQueryHandler : IRequestHandler + { + private readonly IssueDbContext _context; + + public IssueQueryHandler(IssueDbContext context) + { + _context = context; + } + + public async Task Handle(IssueQuery query, CancellationToken cancellationToken) + { + return await _context.Issues + .Include(i => i.Project) + .Include(i => i.Tracker) + .Include(i => i.Status) + .Include(i => i.Priority) + .Include(i => i.Author) + .Include(i => i.Assignee) + .AsNoTracking() + .Where(i => i.Id == query.Id) + .Select(i => new IssueView + { + Id = i.Id, + Number = i.Number, + Title = i.Title, + Description = i.Description, + CreateDate = i.CreateDate, + UpdateDate = i.UpdateDate, + StartDate = i.StartDate, + DueDate = i.DueDate, + TrackerName = i.Tracker.Name, + StatusName = i.Status.Name, + PriorityName = i.Priority.Name, + AuthorName = i.Author.Name, + AssigneeName = i.Assignee.Name + }) + .SingleOrDefaultAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueStatus.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueStatus.cs new file mode 100644 index 0000000..8136434 --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/IssueStatus.cs @@ -0,0 +1,10 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class IssueStatus + { + public Guid Id { get; } + public string Name { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Project.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Project.cs new file mode 100644 index 0000000..0fbbfeb --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Project.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class Project + { + public Guid Id { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/ProjectDbContext.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/ProjectDbContext.cs new file mode 100644 index 0000000..59272ee --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/ProjectDbContext.cs @@ -0,0 +1,126 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + public sealed class ProjectDbContext : DbContext + { + public ProjectDbContext(DbContextOptions options) : base(options) { } + + internal DbSet Projects { get; set; } + internal DbSet Trackers { get; set; } + internal DbSet IssueStatuses { get; set; } + internal DbSet IssuePriorities { get; set; } + internal DbSet Users { get; set; } + internal DbSet Issues { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => + { + builder.ToTable("Project"); + builder.HasKey(p => p.Id); + builder.Property(p => p.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Tracker"); + builder.HasKey(t => t.Id); + builder.Property(t => t.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("IssueStatus"); + builder.HasKey(@is => @is.Id); + builder.Property(@is => @is.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("IssuePriority"); + builder.HasKey(ip => ip.Id); + builder.Property(ip => ip.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("User"); + builder.HasKey(u => u.Id); + builder.Property(u => u.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder.ToTable("Issue"); + builder.HasKey(i => i.Id); + builder.Property(i => i.Id) + .HasColumnName("Id") + .ValueGeneratedNever(); + builder.Property(i => i.Title) + .HasColumnName("Title") + .IsRequired(); + builder.Property(i => i.Description) + .HasColumnName("Description") + .IsRequired(); + builder.Property(i => i.CreateDate) + .HasColumnName("CreateDate") + .IsRequired(); + builder.Property(i => i.StartDate) + .HasColumnName("StartDate"); + builder.Property(i => i.DueDate) + .HasColumnName("DueDate"); + builder.Property(i => i.TrackerId) + .HasColumnName("TrackerId") + .IsRequired(); + builder.Property(i => i.StatusId) + .HasColumnName("StatusId") + .IsRequired(); + builder.Property(i => i.PriorityId) + .HasColumnName("PriorityId") + .IsRequired(); + builder.Property(i => i.AuthorId) + .HasColumnName("AuthorId") + .IsRequired(); + builder.Property(i => i.AssigneeId) + .HasColumnName("AssigneeId") + .IsRequired(); + builder.Property("_concurrencyStamp") + .HasColumnName("ConcurrencyStamp") + .IsConcurrencyToken(); + builder.HasOne(i => i.Tracker) + .WithMany() + .HasForeignKey(i => i.TrackerId) + .HasPrincipalKey(t => t.Id); + builder.HasOne(i => i.Status) + .WithMany() + .HasForeignKey(i => i.StatusId) + .HasPrincipalKey(@is => @is.Id); + builder.HasOne(i => i.Priority) + .WithMany() + .HasForeignKey(i => i.PriorityId) + .HasPrincipalKey(ip => ip.Id); + builder.HasOne(i => i.Author) + .WithMany() + .HasForeignKey(i => i.AuthorId) + .HasPrincipalKey(a => a.Id); + builder.HasOne(i => i.Assignee) + .WithMany() + .HasForeignKey(i => i.AssigneeId) + .HasPrincipalKey(p => p.Id); + }); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Tracker.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Tracker.cs new file mode 100644 index 0000000..e4c0cd2 --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/Tracker.cs @@ -0,0 +1,10 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class Tracker + { + public Guid Id { get; } + public string Name { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/User.cs b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/User.cs new file mode 100644 index 0000000..984508f --- /dev/null +++ b/src/ProjectManagementSystem.Queries.Infrastructure/User/Issues/User.cs @@ -0,0 +1,10 @@ +using System; + +namespace ProjectManagementSystem.Queries.Infrastructure.User.Issues +{ + internal sealed class User + { + public Guid Id { get; } + public string Name { get; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries/Admin/Permissions/PermissionView.cs b/src/ProjectManagementSystem.Queries/Admin/Permissions/PermissionView.cs new file mode 100644 index 0000000..f5361ae --- /dev/null +++ b/src/ProjectManagementSystem.Queries/Admin/Permissions/PermissionView.cs @@ -0,0 +1,7 @@ +namespace ProjectManagementSystem.Queries.Admin.Permissions +{ + public sealed class PermissionView + { + + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListItemView.cs b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListItemView.cs new file mode 100644 index 0000000..175ad30 --- /dev/null +++ b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListItemView.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Queries.Admin.Roles +{ + public sealed class RoleListItemView + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListQuery.cs b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListQuery.cs new file mode 100644 index 0000000..b934245 --- /dev/null +++ b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleListQuery.cs @@ -0,0 +1,7 @@ +namespace ProjectManagementSystem.Queries.Admin.Roles +{ + public sealed class RoleListQuery : PageQuery + { + public RoleListQuery(int offset, int limit) : base(offset, limit) { } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries/Admin/Roles/RoleQuery.cs b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleQuery.cs new file mode 100644 index 0000000..672fc79 --- /dev/null +++ b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleQuery.cs @@ -0,0 +1,15 @@ +using System; +using MediatR; + +namespace ProjectManagementSystem.Queries.Admin.Roles +{ + public sealed class RoleQuery : IRequest + { + public Guid Id { get; } + + public RoleQuery(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.Queries/Admin/Roles/RoleView.cs b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleView.cs new file mode 100644 index 0000000..e929240 --- /dev/null +++ b/src/ProjectManagementSystem.Queries/Admin/Roles/RoleView.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.Queries.Admin.Roles +{ + public sealed class RoleView + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Authorization/PermissionAuthorizationHandler.cs b/src/ProjectManagementSystem.WebApi/Authorization/PermissionAuthorizationHandler.cs new file mode 100644 index 0000000..d2a492c --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Authorization/PermissionAuthorizationHandler.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace ProjectManagementSystem.WebApi.Authorization +{ + public class PermissionAuthorizationHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + { + //var a = context.HttpContext.Request.RouteValues.; + + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Authorization/PermissionRequirement.cs b/src/ProjectManagementSystem.WebApi/Authorization/PermissionRequirement.cs new file mode 100644 index 0000000..09d888b --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Authorization/PermissionRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace ProjectManagementSystem.WebApi.Authorization +{ + public sealed class PermissionRequirement : IAuthorizationRequirement + { + public string Permission { get; } + + public PermissionRequirement(string permission) + { + Permission = permission; + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Controllers/Admin/PermissionsController.cs b/src/ProjectManagementSystem.WebApi/Controllers/Admin/PermissionsController.cs new file mode 100644 index 0000000..cd8dc13 --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Controllers/Admin/PermissionsController.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ProjectManagementSystem.WebApi.Controllers.Admin +{ + [ApiController] + public sealed class PermissionsController : ControllerBase + { + + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Controllers/Admin/RolesController.cs b/src/ProjectManagementSystem.WebApi/Controllers/Admin/RolesController.cs new file mode 100644 index 0000000..58d1e0d --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Controllers/Admin/RolesController.cs @@ -0,0 +1,81 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ProjectManagementSystem.Domain.Admin.CreateRoles; +using ProjectManagementSystem.Queries.Admin.Roles; +using ProjectManagementSystem.WebApi.Exceptions; +using ProjectManagementSystem.WebApi.Models.Admin.Roles; + +namespace ProjectManagementSystem.WebApi.Controllers.Admin +{ + [Authorize(Roles = "Admin")] + [ApiController] + public class RolesController : ControllerBase + { + [HttpPost("admin/roles")] + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(typeof(ProblemDetails), 409)] + public async Task Create( + CancellationToken cancellationToken, + [FromBody] CreateRoleBindModel model, + [FromServices] IRoleRepository roleRepository, + [FromServices] IPermissionRepository permissionRepository) + { + var role = await roleRepository.Get(model.Id, cancellationToken); + + if (role != null) + throw new ApiException(HttpStatusCode.Conflict, ErrorCode.RoleAlreadyExists, + "Role already exists with other parameters"); + + role = new Role(model.Id, model.Name); + + foreach (var permissionModel in model.Permissions) + { + var permission = await permissionRepository.Get(permissionModel.Id, cancellationToken); + + if (permission == null) + throw new ApiException(HttpStatusCode.NotFound, ErrorCode.TrackerNotFound, "Tracker not found"); + + var rolePermission = new RolePermission(model.Id, permission.Id); + + role.AddRolePermission(rolePermission); + } + + await roleRepository.Save(role); + + return CreatedAtRoute("GetRoleAdminRoute", new {id = role.Id}, null); + } + + [HttpGet("admin/roles")] + [ProducesResponseType(typeof(RoleListViewModel), 200)] + public async Task Find( + CancellationToken cancellationToken, + [FromQuery] QueryRoleBindModel model, + [FromServices] IMediator mediator) + { + return Ok(await mediator.Send(new RoleListQuery(model.Offset, model.Limit), cancellationToken)); + } + + [HttpGet("admin/roles/{id}", Name = "GetRoleAdminRoute")] + [ProducesResponseType(typeof(RoleViewModel), 200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + public async Task Get( + CancellationToken cancellationToken, + [FromRoute] Guid id, + [FromServices] IMediator mediator) + { + var role = await mediator.Send(new RoleQuery(id), cancellationToken); + + if (role == null) + throw new ApiException(HttpStatusCode.NotFound, ErrorCode.IssueStatusNotFound, + "Issue status not found"); + + return Ok(role); + } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/AddPermissionBindModel.cs b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/AddPermissionBindModel.cs new file mode 100644 index 0000000..017ccc7 --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/AddPermissionBindModel.cs @@ -0,0 +1,9 @@ +using System; + +namespace ProjectManagementSystem.WebApi.Models.Admin.Roles +{ + public sealed class AddPermissionBindModel + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/CreateRoleBindModel.cs b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/CreateRoleBindModel.cs new file mode 100644 index 0000000..d98f122 --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/CreateRoleBindModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace ProjectManagementSystem.WebApi.Models.Admin.Roles +{ + public sealed class CreateRoleBindModel + { + public Guid Id { get; private set; } + public string Name { get; private set; } + public IEnumerable Permissions { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/QueryRoleBindModel.cs b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/QueryRoleBindModel.cs new file mode 100644 index 0000000..e7203f6 --- /dev/null +++ b/src/ProjectManagementSystem.WebApi/Models/Admin/Roles/QueryRoleBindModel.cs @@ -0,0 +1,7 @@ +namespace ProjectManagementSystem.WebApi.Models.Admin.Roles +{ + public sealed class QueryRoleBindModel : QueryPageBindModel + { + + } +} \ No newline at end of file