Spaces:
Running
Running
User commited on
Commit ·
72f2be7
1
Parent(s): 631e3a2
fix: match the menu access and permissions
Browse files- TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -46
- TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs +0 -33
- TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs +0 -31
- TaskTrackingSystem.Database/AppDbContextModels/RoleMenusLegacy.cs +0 -25
- TaskTrackingSystem.Shared/Models/Menu/AccessPermissionDto.cs +2 -2
- TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +9 -0
- TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs +34 -2
- TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs +28 -6
- TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +40 -2
- TaskTrackingSystem.WebApi/Features/Task/TaskController.cs +23 -1
- TaskTrackingSystem.WebApi/Features/User/UserController.cs +19 -4
- TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs +62 -0
- TaskTrackingSystem.WebApi/Program.cs +1 -0
- TaskTrackingSystem.WebApp/Components/CustomAuthenticationStateProvider.cs +3 -2
- TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +2 -2
- TaskTrackingSystem.WebApp/Components/Pages/Home.razor +10 -3
- TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor +13 -4
- TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor +13 -5
- TaskTrackingSystem.WebApp/Components/Pages/Projects.razor +34 -12
- TaskTrackingSystem.WebApp/Components/Pages/Roles.razor +55 -30
- TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor +13 -5
- TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor +52 -20
- TaskTrackingSystem.WebApp/Components/Pages/Users.razor +29 -10
- TaskTrackingSystem.WebApp/MenuAuthorizationService.cs +161 -93
- TaskTrackingSystem.WebApp/UserSessionState.cs +12 -9
- cleanup-legacy-access-schema.sql +41 -0
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs
CHANGED
|
@@ -23,10 +23,6 @@ public partial class AppDbContext : DbContext
|
|
| 23 |
|
| 24 |
public virtual DbSet<Menu> Menus { get; set; }
|
| 25 |
|
| 26 |
-
public virtual DbSet<MenuAdmin> MenuAdmins { get; set; }
|
| 27 |
-
|
| 28 |
-
public virtual DbSet<MenuAdminDetail> MenuAdminDetails { get; set; }
|
| 29 |
-
|
| 30 |
public virtual DbSet<Permission> Permissions { get; set; }
|
| 31 |
|
| 32 |
public virtual DbSet<Project> Projects { get; set; }
|
|
@@ -37,8 +33,6 @@ public partial class AppDbContext : DbContext
|
|
| 37 |
|
| 38 |
public virtual DbSet<RoleMenu> RoleMenus { get; set; }
|
| 39 |
|
| 40 |
-
public virtual DbSet<RoleMenusLegacy> RoleMenusLegacies { get; set; }
|
| 41 |
-
|
| 42 |
public virtual DbSet<RolePermission> RolePermissions { get; set; }
|
| 43 |
|
| 44 |
public virtual DbSet<Task> Tasks { get; set; }
|
|
@@ -132,31 +126,6 @@ public partial class AppDbContext : DbContext
|
|
| 132 |
.HasConstraintName("FK_Menus_ParentMenu");
|
| 133 |
});
|
| 134 |
|
| 135 |
-
modelBuilder.Entity<MenuAdmin>(entity =>
|
| 136 |
-
{
|
| 137 |
-
entity.HasKey(e => e.AdminMenuId);
|
| 138 |
-
|
| 139 |
-
entity.Property(e => e.AdminMenuId).HasMaxLength(50);
|
| 140 |
-
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 141 |
-
entity.Property(e => e.Icon).HasMaxLength(50);
|
| 142 |
-
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
| 143 |
-
entity.Property(e => e.MenuName).HasMaxLength(100);
|
| 144 |
-
entity.Property(e => e.MenuUrl).HasMaxLength(200);
|
| 145 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 146 |
-
entity.Property(e => e.ParentCode).HasMaxLength(50);
|
| 147 |
-
});
|
| 148 |
-
|
| 149 |
-
modelBuilder.Entity<MenuAdminDetail>(entity =>
|
| 150 |
-
{
|
| 151 |
-
entity.Property(e => e.MenuAdminDetailId).HasMaxLength(50);
|
| 152 |
-
entity.Property(e => e.ActionName).HasMaxLength(50);
|
| 153 |
-
entity.Property(e => e.ApiName).HasMaxLength(100);
|
| 154 |
-
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 155 |
-
entity.Property(e => e.MenuDetailCode).HasMaxLength(50);
|
| 156 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 157 |
-
entity.Property(e => e.ParentMenuCode).HasMaxLength(50);
|
| 158 |
-
});
|
| 159 |
-
|
| 160 |
modelBuilder.Entity<Permission>(entity =>
|
| 161 |
{
|
| 162 |
entity.HasIndex(e => e.MenuId, "IX_Permissions_MenuId");
|
|
@@ -252,21 +221,6 @@ public partial class AppDbContext : DbContext
|
|
| 252 |
.HasConstraintName("FK_RoleMenus_Roles");
|
| 253 |
});
|
| 254 |
|
| 255 |
-
modelBuilder.Entity<RoleMenusLegacy>(entity =>
|
| 256 |
-
{
|
| 257 |
-
entity.HasKey(e => e.RoleMenuId).HasName("PK_RoleMenus");
|
| 258 |
-
|
| 259 |
-
entity.ToTable("RoleMenus_Legacy");
|
| 260 |
-
|
| 261 |
-
entity.HasIndex(e => e.RoleId, "IX_RoleMenus_RoleId");
|
| 262 |
-
|
| 263 |
-
entity.Property(e => e.RoleMenuId).HasMaxLength(50);
|
| 264 |
-
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 265 |
-
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
| 266 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 267 |
-
entity.Property(e => e.RoleCode).HasMaxLength(50);
|
| 268 |
-
});
|
| 269 |
-
|
| 270 |
modelBuilder.Entity<RolePermission>(entity =>
|
| 271 |
{
|
| 272 |
entity.HasIndex(e => e.PermissionId, "IX_RolePermissions_PermissionId");
|
|
|
|
| 23 |
|
| 24 |
public virtual DbSet<Menu> Menus { get; set; }
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
public virtual DbSet<Permission> Permissions { get; set; }
|
| 27 |
|
| 28 |
public virtual DbSet<Project> Projects { get; set; }
|
|
|
|
| 33 |
|
| 34 |
public virtual DbSet<RoleMenu> RoleMenus { get; set; }
|
| 35 |
|
|
|
|
|
|
|
| 36 |
public virtual DbSet<RolePermission> RolePermissions { get; set; }
|
| 37 |
|
| 38 |
public virtual DbSet<Task> Tasks { get; set; }
|
|
|
|
| 126 |
.HasConstraintName("FK_Menus_ParentMenu");
|
| 127 |
});
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
modelBuilder.Entity<Permission>(entity =>
|
| 130 |
{
|
| 131 |
entity.HasIndex(e => e.MenuId, "IX_Permissions_MenuId");
|
|
|
|
| 221 |
.HasConstraintName("FK_RoleMenus_Roles");
|
| 222 |
});
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
modelBuilder.Entity<RolePermission>(entity =>
|
| 225 |
{
|
| 226 |
entity.HasIndex(e => e.PermissionId, "IX_RolePermissions_PermissionId");
|
TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
using System;
|
| 2 |
-
using System.Collections.Generic;
|
| 3 |
-
|
| 4 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
-
|
| 6 |
-
public partial class MenuAdmin
|
| 7 |
-
{
|
| 8 |
-
public string AdminMenuId { get; set; } = null!;
|
| 9 |
-
|
| 10 |
-
public string MenuCode { get; set; } = null!;
|
| 11 |
-
|
| 12 |
-
public string ParentCode { get; set; } = null!;
|
| 13 |
-
|
| 14 |
-
public string MenuName { get; set; } = null!;
|
| 15 |
-
|
| 16 |
-
public string? MenuUrl { get; set; }
|
| 17 |
-
|
| 18 |
-
public bool Visible { get; set; }
|
| 19 |
-
|
| 20 |
-
public int OrderNo { get; set; }
|
| 21 |
-
|
| 22 |
-
public string? Icon { get; set; }
|
| 23 |
-
|
| 24 |
-
public int DelFlag { get; set; }
|
| 25 |
-
|
| 26 |
-
public string? CreatedUserId { get; set; }
|
| 27 |
-
|
| 28 |
-
public DateTime CreatedDateTime { get; set; }
|
| 29 |
-
|
| 30 |
-
public string? ModifiedUserId { get; set; }
|
| 31 |
-
|
| 32 |
-
public DateTime? ModifiedDateTime { get; set; }
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
using System;
|
| 2 |
-
using System.Collections.Generic;
|
| 3 |
-
|
| 4 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
-
|
| 6 |
-
public partial class MenuAdminDetail
|
| 7 |
-
{
|
| 8 |
-
public string MenuAdminDetailId { get; set; } = null!;
|
| 9 |
-
|
| 10 |
-
public string MenuDetailCode { get; set; } = null!;
|
| 11 |
-
|
| 12 |
-
public string ParentMenuCode { get; set; } = null!;
|
| 13 |
-
|
| 14 |
-
public string ActionName { get; set; } = null!;
|
| 15 |
-
|
| 16 |
-
public string ApiName { get; set; } = null!;
|
| 17 |
-
|
| 18 |
-
public bool Visible { get; set; }
|
| 19 |
-
|
| 20 |
-
public int OrderNo { get; set; }
|
| 21 |
-
|
| 22 |
-
public int DelFlag { get; set; }
|
| 23 |
-
|
| 24 |
-
public string? CreatedUserId { get; set; }
|
| 25 |
-
|
| 26 |
-
public DateTime CreatedDateTime { get; set; }
|
| 27 |
-
|
| 28 |
-
public string? ModifiedUserId { get; set; }
|
| 29 |
-
|
| 30 |
-
public DateTime? ModifiedDateTime { get; set; }
|
| 31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.Database/AppDbContextModels/RoleMenusLegacy.cs
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
using System;
|
| 2 |
-
using System.Collections.Generic;
|
| 3 |
-
|
| 4 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
-
|
| 6 |
-
public partial class RoleMenusLegacy
|
| 7 |
-
{
|
| 8 |
-
public string RoleMenuId { get; set; } = null!;
|
| 9 |
-
|
| 10 |
-
public string RoleCode { get; set; } = null!;
|
| 11 |
-
|
| 12 |
-
public string MenuCode { get; set; } = null!;
|
| 13 |
-
|
| 14 |
-
public int DelFlag { get; set; }
|
| 15 |
-
|
| 16 |
-
public string? CreatedUserId { get; set; }
|
| 17 |
-
|
| 18 |
-
public DateTime CreatedDateTime { get; set; }
|
| 19 |
-
|
| 20 |
-
public string? ModifiedUserId { get; set; }
|
| 21 |
-
|
| 22 |
-
public DateTime? ModifiedDateTime { get; set; }
|
| 23 |
-
|
| 24 |
-
public long RoleId { get; set; }
|
| 25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.Shared/Models/Menu/AccessPermissionDto.cs
CHANGED
|
@@ -4,8 +4,8 @@ namespace TaskTrackingSystem.Shared.Models.Menu
|
|
| 4 |
{
|
| 5 |
public class AccessPermissionDto
|
| 6 |
{
|
| 7 |
-
public string
|
| 8 |
-
public string
|
| 9 |
public string ParentMenuCode { get; set; } = string.Empty;
|
| 10 |
public string ActionName { get; set; } = string.Empty;
|
| 11 |
public string ApiName { get; set; } = string.Empty;
|
|
|
|
| 4 |
{
|
| 5 |
public class AccessPermissionDto
|
| 6 |
{
|
| 7 |
+
public string PermissionId { get; set; } = string.Empty;
|
| 8 |
+
public string PermissionCode { get; set; } = string.Empty;
|
| 9 |
public string ParentMenuCode { get; set; } = string.Empty;
|
| 10 |
public string ActionName { get; set; } = string.Empty;
|
| 11 |
public string ApiName { get; set; } = string.Empty;
|
TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs
CHANGED
|
@@ -50,6 +50,15 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 50 |
return Ok(menus);
|
| 51 |
}
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
/// <summary>
|
| 54 |
/// Returns a lightweight version string for the current user's role permissions.
|
| 55 |
/// Clients poll this to detect when an admin has changed permissions without fetching the full menu list.
|
|
|
|
| 50 |
return Ok(menus);
|
| 51 |
}
|
| 52 |
|
| 53 |
+
[HttpGet("access-codes")]
|
| 54 |
+
public async Task<ActionResult<IEnumerable<string>>> GetCurrentAccessCodes()
|
| 55 |
+
{
|
| 56 |
+
var roleId = User.GetRoleId();
|
| 57 |
+
var roleName = User.GetRoleName() ?? string.Empty;
|
| 58 |
+
var codes = await _menuService.GetCurrentAccessCodesAsync(roleId, roleName);
|
| 59 |
+
return Ok(codes);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
/// <summary>
|
| 63 |
/// Returns a lightweight version string for the current user's role permissions.
|
| 64 |
/// Clients poll this to detect when an admin has changed permissions without fetching the full menu list.
|
TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs
CHANGED
|
@@ -261,8 +261,8 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 261 |
{
|
| 262 |
dto.Permissions.Add(new AccessPermissionDto
|
| 263 |
{
|
| 264 |
-
|
| 265 |
-
|
| 266 |
ParentMenuCode = menuLookup.TryGetValue(permission.MenuId, out var parentMenu) ? parentMenu.MenuCode : string.Empty,
|
| 267 |
ActionName = permission.ActionName,
|
| 268 |
ApiName = permission.ApiName,
|
|
@@ -274,6 +274,38 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 274 |
|
| 275 |
return menuDtos;
|
| 276 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
| 278 |
}
|
| 279 |
|
|
|
|
| 261 |
{
|
| 262 |
dto.Permissions.Add(new AccessPermissionDto
|
| 263 |
{
|
| 264 |
+
PermissionId = permission.PermissionId.ToString(),
|
| 265 |
+
PermissionCode = permission.PermissionCode,
|
| 266 |
ParentMenuCode = menuLookup.TryGetValue(permission.MenuId, out var parentMenu) ? parentMenu.MenuCode : string.Empty,
|
| 267 |
ActionName = permission.ActionName,
|
| 268 |
ApiName = permission.ApiName,
|
|
|
|
| 274 |
|
| 275 |
return menuDtos;
|
| 276 |
}
|
| 277 |
+
|
| 278 |
+
public async Task<List<string>> GetCurrentAccessCodesAsync(long roleId, string roleName)
|
| 279 |
+
{
|
| 280 |
+
if (roleId <= 0 && !string.IsNullOrWhiteSpace(roleName))
|
| 281 |
+
{
|
| 282 |
+
var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true);
|
| 283 |
+
if (role != null)
|
| 284 |
+
{
|
| 285 |
+
roleId = role.Id;
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
if (roleId <= 0)
|
| 290 |
+
{
|
| 291 |
+
return new List<string>();
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
var menuCodes = await _db.RoleMenus
|
| 295 |
+
.Where(rm => rm.RoleId == roleId && !rm.IsDeleted)
|
| 296 |
+
.Select(rm => rm.Menu.MenuCode)
|
| 297 |
+
.ToListAsync();
|
| 298 |
+
|
| 299 |
+
var permissionCodes = await _db.RolePermissions
|
| 300 |
+
.Where(rp => rp.RoleId == roleId && !rp.IsDeleted)
|
| 301 |
+
.Select(rp => rp.Permission.PermissionCode)
|
| 302 |
+
.ToListAsync();
|
| 303 |
+
|
| 304 |
+
return menuCodes
|
| 305 |
+
.Concat(permissionCodes)
|
| 306 |
+
.Distinct(StringComparer.OrdinalIgnoreCase)
|
| 307 |
+
.ToList();
|
| 308 |
+
}
|
| 309 |
}
|
| 310 |
}
|
| 311 |
|
TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs
CHANGED
|
@@ -14,10 +14,12 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 14 |
public class ProjectController : ControllerBase
|
| 15 |
{
|
| 16 |
private readonly ProjectService _projectService;
|
|
|
|
| 17 |
|
| 18 |
-
public ProjectController(ProjectService projectService)
|
| 19 |
{
|
| 20 |
_projectService = projectService;
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
[HttpGet]
|
|
@@ -39,27 +41,39 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 39 |
}
|
| 40 |
|
| 41 |
[HttpPost]
|
| 42 |
-
[Authorize(Roles = "Admin")]
|
| 43 |
public async Task<ActionResult<Result<ProjectDto>>> CreateProject([FromBody] CreateProjectDto createProjectDto)
|
| 44 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
long? currentUserId = User.GetUserId();
|
| 46 |
var result = await _projectService.CreateProjectAsync(createProjectDto, currentUserId);
|
| 47 |
return StatusCode(result.StatusCode, result);
|
| 48 |
}
|
| 49 |
|
| 50 |
[HttpPut("{id}")]
|
| 51 |
-
[Authorize(Roles = "Admin")]
|
| 52 |
public async Task<ActionResult<Result>> UpdateProject(long id, [FromBody] UpdateProjectDto updateProjectDto)
|
| 53 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
long? currentUserId = User.GetUserId();
|
| 55 |
var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, currentUserId);
|
| 56 |
return StatusCode(result.StatusCode, result);
|
| 57 |
}
|
| 58 |
|
| 59 |
[HttpDelete("{id}")]
|
| 60 |
-
[Authorize(Roles = "Admin")]
|
| 61 |
public async Task<ActionResult<Result>> DeleteProject(long id)
|
| 62 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
var result = await _projectService.SoftDeleteProjectAsync(id);
|
| 64 |
return StatusCode(result.StatusCode, result);
|
| 65 |
}
|
|
@@ -72,9 +86,13 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 72 |
}
|
| 73 |
|
| 74 |
[HttpPost("{id}/members")]
|
| 75 |
-
[Authorize(Roles = "Admin")]
|
| 76 |
public async Task<IActionResult> AssignProjectMembers(long id, [FromBody] AssignMembersDto dto)
|
| 77 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetUserId());
|
| 79 |
if (!result.IsSuccess)
|
| 80 |
{
|
|
@@ -84,9 +102,13 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 84 |
}
|
| 85 |
|
| 86 |
[HttpDelete("{id}/members/{userId}")]
|
| 87 |
-
[Authorize(Roles = "Admin")]
|
| 88 |
public async Task<IActionResult> RemoveProjectMember(long id, long userId)
|
| 89 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetUserId());
|
| 91 |
if (!result.IsSuccess)
|
| 92 |
{
|
|
|
|
| 14 |
public class ProjectController : ControllerBase
|
| 15 |
{
|
| 16 |
private readonly ProjectService _projectService;
|
| 17 |
+
private readonly PermissionAuthorizationService _permissionAuthorizationService;
|
| 18 |
|
| 19 |
+
public ProjectController(ProjectService projectService, PermissionAuthorizationService permissionAuthorizationService)
|
| 20 |
{
|
| 21 |
_projectService = projectService;
|
| 22 |
+
_permissionAuthorizationService = permissionAuthorizationService;
|
| 23 |
}
|
| 24 |
|
| 25 |
[HttpGet]
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
[HttpPost]
|
|
|
|
| 44 |
public async Task<ActionResult<Result<ProjectDto>>> CreateProject([FromBody] CreateProjectDto createProjectDto)
|
| 45 |
{
|
| 46 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Project", "Create"))
|
| 47 |
+
{
|
| 48 |
+
return Forbid();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
long? currentUserId = User.GetUserId();
|
| 52 |
var result = await _projectService.CreateProjectAsync(createProjectDto, currentUserId);
|
| 53 |
return StatusCode(result.StatusCode, result);
|
| 54 |
}
|
| 55 |
|
| 56 |
[HttpPut("{id}")]
|
|
|
|
| 57 |
public async Task<ActionResult<Result>> UpdateProject(long id, [FromBody] UpdateProjectDto updateProjectDto)
|
| 58 |
{
|
| 59 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Project", "Update"))
|
| 60 |
+
{
|
| 61 |
+
return Forbid();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
long? currentUserId = User.GetUserId();
|
| 65 |
var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, currentUserId);
|
| 66 |
return StatusCode(result.StatusCode, result);
|
| 67 |
}
|
| 68 |
|
| 69 |
[HttpDelete("{id}")]
|
|
|
|
| 70 |
public async Task<ActionResult<Result>> DeleteProject(long id)
|
| 71 |
{
|
| 72 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Project", "Delete"))
|
| 73 |
+
{
|
| 74 |
+
return Forbid();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
var result = await _projectService.SoftDeleteProjectAsync(id);
|
| 78 |
return StatusCode(result.StatusCode, result);
|
| 79 |
}
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
[HttpPost("{id}/members")]
|
|
|
|
| 89 |
public async Task<IActionResult> AssignProjectMembers(long id, [FromBody] AssignMembersDto dto)
|
| 90 |
{
|
| 91 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Project", "Update"))
|
| 92 |
+
{
|
| 93 |
+
return Forbid();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
var result = await _projectService.AssignMembersToProjectAsync(id, dto, User.GetUserId());
|
| 97 |
if (!result.IsSuccess)
|
| 98 |
{
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
[HttpDelete("{id}/members/{userId}")]
|
|
|
|
| 105 |
public async Task<IActionResult> RemoveProjectMember(long id, long userId)
|
| 106 |
{
|
| 107 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Project", "Update"))
|
| 108 |
+
{
|
| 109 |
+
return Forbid();
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
var result = await _projectService.RemoveMemberFromProjectAsync(id, userId, User.GetUserId());
|
| 113 |
if (!result.IsSuccess)
|
| 114 |
{
|
TaskTrackingSystem.WebApi/Features/Role/RoleController.cs
CHANGED
|
@@ -2,24 +2,32 @@ using Microsoft.AspNetCore.Mvc;
|
|
| 2 |
using Microsoft.AspNetCore.Authorization;
|
| 3 |
using TaskTrackingSystem.Shared;
|
| 4 |
using TaskTrackingSystem.Shared.Models.Role;
|
|
|
|
| 5 |
|
| 6 |
namespace TaskTrackingSystem.WebApi.Features.Role
|
| 7 |
{
|
| 8 |
[Route("api/[controller]")]
|
| 9 |
[ApiController]
|
| 10 |
-
[Authorize
|
| 11 |
public class RoleController : ControllerBase
|
| 12 |
{
|
| 13 |
private readonly RoleService _roleService;
|
|
|
|
| 14 |
|
| 15 |
-
public RoleController(RoleService roleService)
|
| 16 |
{
|
| 17 |
_roleService = roleService;
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
[HttpGet]
|
| 21 |
public async Task<ActionResult<IEnumerable<RoleDto>>> GetRoles()
|
| 22 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
var roles = await _roleService.GetAllRolesAsync();
|
| 24 |
return Ok(roles);
|
| 25 |
}
|
|
@@ -27,6 +35,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 27 |
[HttpGet("{id}")]
|
| 28 |
public async Task<ActionResult<RoleDto>> GetRole(long id)
|
| 29 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
var role = await _roleService.GetRoleByIdAsync(id);
|
| 31 |
if (role == null)
|
| 32 |
{
|
|
@@ -38,6 +51,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 38 |
[HttpPost]
|
| 39 |
public async Task<ActionResult<Result<RoleDto>>> CreateRole([FromBody] CreateRoleDto createRoleDto)
|
| 40 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
long? currentUserId = null;
|
| 42 |
var result = await _roleService.CreateRoleAsync(createRoleDto, currentUserId);
|
| 43 |
return StatusCode(result.StatusCode, result);
|
|
@@ -46,6 +64,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 46 |
[HttpPut("{id}")]
|
| 47 |
public async Task<ActionResult<Result>> UpdateRole(long id, [FromBody] UpdateRoleDto updateRoleDto)
|
| 48 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
long? currentUserId = null;
|
| 50 |
var result = await _roleService.UpdateRoleAsync(id, updateRoleDto, currentUserId);
|
| 51 |
return StatusCode(result.StatusCode, result);
|
|
@@ -54,6 +77,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 54 |
[HttpDelete("{id}")]
|
| 55 |
public async Task<ActionResult<Result>> DeleteRole(long id)
|
| 56 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
var result = await _roleService.SoftDeleteRoleAsync(id);
|
| 58 |
return StatusCode(result.StatusCode, result);
|
| 59 |
}
|
|
@@ -63,6 +91,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 63 |
[HttpGet("{id}/access")]
|
| 64 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedAccessCodes(long id)
|
| 65 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
var result = await _roleService.GetAssignedAccessCodesByRoleIdAsync(id);
|
| 67 |
if (!result.IsSuccess)
|
| 68 |
{
|
|
@@ -75,6 +108,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 75 |
[HttpPost("{id}/access")]
|
| 76 |
public async Task<IActionResult> AssignAccess(long id, [FromBody] AssignAccessDto dto)
|
| 77 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
var result = await _roleService.AssignAccessToRoleAsync(id, dto);
|
| 79 |
if (!result.IsSuccess)
|
| 80 |
{
|
|
|
|
| 2 |
using Microsoft.AspNetCore.Authorization;
|
| 3 |
using TaskTrackingSystem.Shared;
|
| 4 |
using TaskTrackingSystem.Shared.Models.Role;
|
| 5 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 6 |
|
| 7 |
namespace TaskTrackingSystem.WebApi.Features.Role
|
| 8 |
{
|
| 9 |
[Route("api/[controller]")]
|
| 10 |
[ApiController]
|
| 11 |
+
[Authorize]
|
| 12 |
public class RoleController : ControllerBase
|
| 13 |
{
|
| 14 |
private readonly RoleService _roleService;
|
| 15 |
+
private readonly PermissionAuthorizationService _permissionAuthorizationService;
|
| 16 |
|
| 17 |
+
public RoleController(RoleService roleService, PermissionAuthorizationService permissionAuthorizationService)
|
| 18 |
{
|
| 19 |
_roleService = roleService;
|
| 20 |
+
_permissionAuthorizationService = permissionAuthorizationService;
|
| 21 |
}
|
| 22 |
|
| 23 |
[HttpGet]
|
| 24 |
public async Task<ActionResult<IEnumerable<RoleDto>>> GetRoles()
|
| 25 |
{
|
| 26 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "List"))
|
| 27 |
+
{
|
| 28 |
+
return Forbid();
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
var roles = await _roleService.GetAllRolesAsync();
|
| 32 |
return Ok(roles);
|
| 33 |
}
|
|
|
|
| 35 |
[HttpGet("{id}")]
|
| 36 |
public async Task<ActionResult<RoleDto>> GetRole(long id)
|
| 37 |
{
|
| 38 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "List"))
|
| 39 |
+
{
|
| 40 |
+
return Forbid();
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
var role = await _roleService.GetRoleByIdAsync(id);
|
| 44 |
if (role == null)
|
| 45 |
{
|
|
|
|
| 51 |
[HttpPost]
|
| 52 |
public async Task<ActionResult<Result<RoleDto>>> CreateRole([FromBody] CreateRoleDto createRoleDto)
|
| 53 |
{
|
| 54 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Create"))
|
| 55 |
+
{
|
| 56 |
+
return Forbid();
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
long? currentUserId = null;
|
| 60 |
var result = await _roleService.CreateRoleAsync(createRoleDto, currentUserId);
|
| 61 |
return StatusCode(result.StatusCode, result);
|
|
|
|
| 64 |
[HttpPut("{id}")]
|
| 65 |
public async Task<ActionResult<Result>> UpdateRole(long id, [FromBody] UpdateRoleDto updateRoleDto)
|
| 66 |
{
|
| 67 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update"))
|
| 68 |
+
{
|
| 69 |
+
return Forbid();
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
long? currentUserId = null;
|
| 73 |
var result = await _roleService.UpdateRoleAsync(id, updateRoleDto, currentUserId);
|
| 74 |
return StatusCode(result.StatusCode, result);
|
|
|
|
| 77 |
[HttpDelete("{id}")]
|
| 78 |
public async Task<ActionResult<Result>> DeleteRole(long id)
|
| 79 |
{
|
| 80 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Delete"))
|
| 81 |
+
{
|
| 82 |
+
return Forbid();
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
var result = await _roleService.SoftDeleteRoleAsync(id);
|
| 86 |
return StatusCode(result.StatusCode, result);
|
| 87 |
}
|
|
|
|
| 91 |
[HttpGet("{id}/access")]
|
| 92 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedAccessCodes(long id)
|
| 93 |
{
|
| 94 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update"))
|
| 95 |
+
{
|
| 96 |
+
return Forbid();
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
var result = await _roleService.GetAssignedAccessCodesByRoleIdAsync(id);
|
| 100 |
if (!result.IsSuccess)
|
| 101 |
{
|
|
|
|
| 108 |
[HttpPost("{id}/access")]
|
| 109 |
public async Task<IActionResult> AssignAccess(long id, [FromBody] AssignAccessDto dto)
|
| 110 |
{
|
| 111 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Role", "Update"))
|
| 112 |
+
{
|
| 113 |
+
return Forbid();
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
var result = await _roleService.AssignAccessToRoleAsync(id, dto);
|
| 117 |
if (!result.IsSuccess)
|
| 118 |
{
|
TaskTrackingSystem.WebApi/Features/Task/TaskController.cs
CHANGED
|
@@ -14,10 +14,12 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 14 |
public class TaskController : ControllerBase
|
| 15 |
{
|
| 16 |
private readonly TaskService _taskService;
|
|
|
|
| 17 |
|
| 18 |
-
public TaskController(TaskService taskService)
|
| 19 |
{
|
| 20 |
_taskService = taskService;
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
[HttpGet]
|
|
@@ -41,6 +43,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 41 |
[HttpPost]
|
| 42 |
public async Task<ActionResult<Result<TaskDto>>> CreateTask([FromBody] CreateTaskDto createTaskDto)
|
| 43 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
long? currentUserId = User.GetUserId();
|
| 45 |
var result = await _taskService.CreateTaskAsync(createTaskDto, currentUserId);
|
| 46 |
return StatusCode(result.StatusCode, result);
|
|
@@ -49,6 +56,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 49 |
[HttpPut("{id}")]
|
| 50 |
public async Task<ActionResult<Result>> UpdateTask(long id, [FromBody] UpdateTaskDto updateTaskDto)
|
| 51 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
long? currentUserId = User.GetUserId();
|
| 53 |
var result = await _taskService.UpdateTaskAsync(id, updateTaskDto, User.GetRoleName(), currentUserId);
|
| 54 |
return StatusCode(result.StatusCode, result);
|
|
@@ -57,6 +69,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 57 |
[HttpDelete("{id}")]
|
| 58 |
public async Task<ActionResult<Result>> DeleteTask(long id)
|
| 59 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
var result = await _taskService.SoftDeleteTaskAsync(id, User.GetRoleName(), User.GetUserId());
|
| 61 |
return StatusCode(result.StatusCode, result);
|
| 62 |
}
|
|
@@ -71,6 +88,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 71 |
[HttpPatch("{id}/status")]
|
| 72 |
public async Task<IActionResult> UpdateTaskStatus(long id, [FromQuery] long statusId)
|
| 73 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleName(), User.GetUserId());
|
| 75 |
if (!result.IsSuccess)
|
| 76 |
{
|
|
|
|
| 14 |
public class TaskController : ControllerBase
|
| 15 |
{
|
| 16 |
private readonly TaskService _taskService;
|
| 17 |
+
private readonly PermissionAuthorizationService _permissionAuthorizationService;
|
| 18 |
|
| 19 |
+
public TaskController(TaskService taskService, PermissionAuthorizationService permissionAuthorizationService)
|
| 20 |
{
|
| 21 |
_taskService = taskService;
|
| 22 |
+
_permissionAuthorizationService = permissionAuthorizationService;
|
| 23 |
}
|
| 24 |
|
| 25 |
[HttpGet]
|
|
|
|
| 43 |
[HttpPost]
|
| 44 |
public async Task<ActionResult<Result<TaskDto>>> CreateTask([FromBody] CreateTaskDto createTaskDto)
|
| 45 |
{
|
| 46 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Task", "Create"))
|
| 47 |
+
{
|
| 48 |
+
return Forbid();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
long? currentUserId = User.GetUserId();
|
| 52 |
var result = await _taskService.CreateTaskAsync(createTaskDto, currentUserId);
|
| 53 |
return StatusCode(result.StatusCode, result);
|
|
|
|
| 56 |
[HttpPut("{id}")]
|
| 57 |
public async Task<ActionResult<Result>> UpdateTask(long id, [FromBody] UpdateTaskDto updateTaskDto)
|
| 58 |
{
|
| 59 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Task", "Update"))
|
| 60 |
+
{
|
| 61 |
+
return Forbid();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
long? currentUserId = User.GetUserId();
|
| 65 |
var result = await _taskService.UpdateTaskAsync(id, updateTaskDto, User.GetRoleName(), currentUserId);
|
| 66 |
return StatusCode(result.StatusCode, result);
|
|
|
|
| 69 |
[HttpDelete("{id}")]
|
| 70 |
public async Task<ActionResult<Result>> DeleteTask(long id)
|
| 71 |
{
|
| 72 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Task", "Delete"))
|
| 73 |
+
{
|
| 74 |
+
return Forbid();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
var result = await _taskService.SoftDeleteTaskAsync(id, User.GetRoleName(), User.GetUserId());
|
| 78 |
return StatusCode(result.StatusCode, result);
|
| 79 |
}
|
|
|
|
| 88 |
[HttpPatch("{id}/status")]
|
| 89 |
public async Task<IActionResult> UpdateTaskStatus(long id, [FromQuery] long statusId)
|
| 90 |
{
|
| 91 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/Task", "Update"))
|
| 92 |
+
{
|
| 93 |
+
return Forbid();
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
var result = await _taskService.UpdateTaskStatusAsync(id, statusId, User.GetRoleName(), User.GetUserId());
|
| 97 |
if (!result.IsSuccess)
|
| 98 |
{
|
TaskTrackingSystem.WebApi/Features/User/UserController.cs
CHANGED
|
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|
| 4 |
using TaskTrackingSystem.Shared;
|
| 5 |
using TaskTrackingSystem.Shared.Models.User;
|
| 6 |
using TaskTrackingSystem.WebApi.Features.User;
|
|
|
|
| 7 |
|
| 8 |
namespace TaskTrackingSystem.WebApi.Features.User
|
| 9 |
{
|
|
@@ -13,10 +14,12 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 13 |
public class UserController : ControllerBase
|
| 14 |
{
|
| 15 |
private readonly UserService _userService;
|
|
|
|
| 16 |
|
| 17 |
-
public UserController(UserService userService)
|
| 18 |
{
|
| 19 |
_userService = userService;
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
[HttpGet]
|
|
@@ -38,27 +41,39 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 38 |
}
|
| 39 |
|
| 40 |
[HttpPost]
|
| 41 |
-
[Authorize(Roles = "Admin")]
|
| 42 |
public async Task<ActionResult<Result<UserDto>>> CreateUser([FromBody] CreateUserDto createUserDto)
|
| 43 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
long? currentUserId = null;
|
| 45 |
var result = await _userService.CreateUserAsync(createUserDto, currentUserId);
|
| 46 |
return StatusCode(result.StatusCode, result);
|
| 47 |
}
|
| 48 |
|
| 49 |
[HttpPut("{id}")]
|
| 50 |
-
[Authorize(Roles = "Admin")]
|
| 51 |
public async Task<ActionResult<Result>> UpdateUser(long id, [FromBody] UpdateUserDto updateUserDto)
|
| 52 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
long? currentUserId = null;
|
| 54 |
var result = await _userService.UpdateUserAsync(id, updateUserDto, currentUserId);
|
| 55 |
return StatusCode(result.StatusCode, result);
|
| 56 |
}
|
| 57 |
|
| 58 |
[HttpDelete("{id}")]
|
| 59 |
-
[Authorize(Roles = "Admin")]
|
| 60 |
public async Task<ActionResult<Result>> DeleteUser(long id)
|
| 61 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
long? loggedInUserId = null;
|
| 63 |
var nameIdentifier = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 64 |
if (nameIdentifier != null && long.TryParse(nameIdentifier, out var parsedId))
|
|
|
|
| 4 |
using TaskTrackingSystem.Shared;
|
| 5 |
using TaskTrackingSystem.Shared.Models.User;
|
| 6 |
using TaskTrackingSystem.WebApi.Features.User;
|
| 7 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 8 |
|
| 9 |
namespace TaskTrackingSystem.WebApi.Features.User
|
| 10 |
{
|
|
|
|
| 14 |
public class UserController : ControllerBase
|
| 15 |
{
|
| 16 |
private readonly UserService _userService;
|
| 17 |
+
private readonly PermissionAuthorizationService _permissionAuthorizationService;
|
| 18 |
|
| 19 |
+
public UserController(UserService userService, PermissionAuthorizationService permissionAuthorizationService)
|
| 20 |
{
|
| 21 |
_userService = userService;
|
| 22 |
+
_permissionAuthorizationService = permissionAuthorizationService;
|
| 23 |
}
|
| 24 |
|
| 25 |
[HttpGet]
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
[HttpPost]
|
|
|
|
| 44 |
public async Task<ActionResult<Result<UserDto>>> CreateUser([FromBody] CreateUserDto createUserDto)
|
| 45 |
{
|
| 46 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/User", "Create"))
|
| 47 |
+
{
|
| 48 |
+
return Forbid();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
long? currentUserId = null;
|
| 52 |
var result = await _userService.CreateUserAsync(createUserDto, currentUserId);
|
| 53 |
return StatusCode(result.StatusCode, result);
|
| 54 |
}
|
| 55 |
|
| 56 |
[HttpPut("{id}")]
|
|
|
|
| 57 |
public async Task<ActionResult<Result>> UpdateUser(long id, [FromBody] UpdateUserDto updateUserDto)
|
| 58 |
{
|
| 59 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/User", "Update"))
|
| 60 |
+
{
|
| 61 |
+
return Forbid();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
long? currentUserId = null;
|
| 65 |
var result = await _userService.UpdateUserAsync(id, updateUserDto, currentUserId);
|
| 66 |
return StatusCode(result.StatusCode, result);
|
| 67 |
}
|
| 68 |
|
| 69 |
[HttpDelete("{id}")]
|
|
|
|
| 70 |
public async Task<ActionResult<Result>> DeleteUser(long id)
|
| 71 |
{
|
| 72 |
+
if (!await _permissionAuthorizationService.CanAccessAsync(User, "api/User", "Delete"))
|
| 73 |
+
{
|
| 74 |
+
return Forbid();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
long? loggedInUserId = null;
|
| 78 |
var nameIdentifier = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 79 |
if (nameIdentifier != null && long.TryParse(nameIdentifier, out var parsedId))
|
TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.EntityFrameworkCore;
|
| 2 |
+
using System.Security.Claims;
|
| 3 |
+
using TaskTrackingSystem.Database.AppDbContextModels;
|
| 4 |
+
|
| 5 |
+
namespace TaskTrackingSystem.WebApi.Infrastructure;
|
| 6 |
+
|
| 7 |
+
public class PermissionAuthorizationService
|
| 8 |
+
{
|
| 9 |
+
private readonly AppDbContext _db;
|
| 10 |
+
|
| 11 |
+
public PermissionAuthorizationService(AppDbContext db)
|
| 12 |
+
{
|
| 13 |
+
_db = db;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
public async Task<bool> CanAccessAsync(ClaimsPrincipal user, string apiName, string actionName)
|
| 17 |
+
{
|
| 18 |
+
if (user.Identity?.IsAuthenticated != true)
|
| 19 |
+
{
|
| 20 |
+
return false;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
if (user.IsInRole("Admin"))
|
| 24 |
+
{
|
| 25 |
+
return true;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
var roleId = await ResolveRoleIdAsync(user);
|
| 29 |
+
if (roleId <= 0)
|
| 30 |
+
{
|
| 31 |
+
return false;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return await _db.RolePermissions
|
| 35 |
+
.AnyAsync(rp =>
|
| 36 |
+
rp.RoleId == roleId &&
|
| 37 |
+
!rp.IsDeleted &&
|
| 38 |
+
!rp.Permission.IsDeleted &&
|
| 39 |
+
rp.Permission.ApiName == apiName &&
|
| 40 |
+
rp.Permission.ActionName == actionName);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
private async Task<long> ResolveRoleIdAsync(ClaimsPrincipal user)
|
| 44 |
+
{
|
| 45 |
+
var roleId = user.GetRoleId();
|
| 46 |
+
if (roleId > 0)
|
| 47 |
+
{
|
| 48 |
+
return roleId;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
var roleName = user.GetRoleName();
|
| 52 |
+
if (string.IsNullOrWhiteSpace(roleName))
|
| 53 |
+
{
|
| 54 |
+
return 0;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return await _db.Roles
|
| 58 |
+
.Where(r => r.Name == roleName && r.IsDeleted != true)
|
| 59 |
+
.Select(r => r.Id)
|
| 60 |
+
.FirstOrDefaultAsync();
|
| 61 |
+
}
|
| 62 |
+
}
|
TaskTrackingSystem.WebApi/Program.cs
CHANGED
|
@@ -30,6 +30,7 @@ builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Task.TaskService>(
|
|
| 30 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Dashboard.DashboardService>();
|
| 31 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Report.ReportService>();
|
| 32 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Menu.MenuService>();
|
|
|
|
| 33 |
builder.Services.AddScoped<IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User>, PasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User>>();
|
| 34 |
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
| 35 |
builder.Services.AddAuthorization();
|
|
|
|
| 30 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Dashboard.DashboardService>();
|
| 31 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Report.ReportService>();
|
| 32 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Menu.MenuService>();
|
| 33 |
+
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Infrastructure.PermissionAuthorizationService>();
|
| 34 |
builder.Services.AddScoped<IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User>, PasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User>>();
|
| 35 |
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
| 36 |
builder.Services.AddAuthorization();
|
TaskTrackingSystem.WebApp/Components/CustomAuthenticationStateProvider.cs
CHANGED
|
@@ -52,14 +52,15 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|
| 52 |
var roleId = user.FindFirst("role_id")?.Value;
|
| 53 |
if (!string.IsNullOrWhiteSpace(roleId))
|
| 54 |
{
|
| 55 |
-
_sessionState.
|
| 56 |
return;
|
| 57 |
}
|
| 58 |
|
| 59 |
var roleName = user.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value;
|
| 60 |
if (!string.IsNullOrWhiteSpace(roleName))
|
| 61 |
{
|
| 62 |
-
_sessionState.
|
| 63 |
}
|
| 64 |
}
|
| 65 |
}
|
|
|
|
|
|
| 52 |
var roleId = user.FindFirst("role_id")?.Value;
|
| 53 |
if (!string.IsNullOrWhiteSpace(roleId))
|
| 54 |
{
|
| 55 |
+
_sessionState.CachedAccessRoleId = $"id:{roleId}";
|
| 56 |
return;
|
| 57 |
}
|
| 58 |
|
| 59 |
var roleName = user.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value;
|
| 60 |
if (!string.IsNullOrWhiteSpace(roleName))
|
| 61 |
{
|
| 62 |
+
_sessionState.CachedAccessRoleId = $"name:{roleName}";
|
| 63 |
}
|
| 64 |
}
|
| 65 |
}
|
| 66 |
+
|
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor
CHANGED
|
@@ -51,7 +51,7 @@
|
|
| 51 |
else
|
| 52 |
{
|
| 53 |
<div class="px-4 py-6 text-xs text-slate-400">
|
| 54 |
-
No
|
| 55 |
</div>
|
| 56 |
}
|
| 57 |
</nav>
|
|
@@ -126,7 +126,7 @@
|
|
| 126 |
}
|
| 127 |
else
|
| 128 |
{
|
| 129 |
-
//
|
| 130 |
foreach (var menu in menus)
|
| 131 |
{
|
| 132 |
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
|
|
|
|
| 51 |
else
|
| 52 |
{
|
| 53 |
<div class="px-4 py-6 text-xs text-slate-400">
|
| 54 |
+
No access items are available for this role.
|
| 55 |
</div>
|
| 56 |
}
|
| 57 |
</nav>
|
|
|
|
| 126 |
}
|
| 127 |
else
|
| 128 |
{
|
| 129 |
+
// Access items are the same — just refresh expanded state for the new route.
|
| 130 |
foreach (var menu in menus)
|
| 131 |
{
|
| 132 |
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
|
TaskTrackingSystem.WebApp/Components/Pages/Home.razor
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
@inject ApiClientService ApiClient
|
| 14 |
@inject IJSRuntime JS
|
| 15 |
@inject AuthenticationStateProvider AuthStateProvider
|
|
|
|
| 16 |
@inject NavigationManager Navigation
|
| 17 |
|
| 18 |
<PageTitle>Dashboard</PageTitle>
|
|
@@ -39,9 +40,12 @@ else
|
|
| 39 |
<p class="text-slate-500 mt-1">Here's what's happening across your workspace today.</p>
|
| 40 |
</div>
|
| 41 |
<div class="flex gap-3">
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
<a href="/reports/tasks" class="inline-flex items-center rounded-xl bg-violet-600 text-white px-4 py-2.5 text-sm font-semibold hover:bg-violet-700 transition-colors shadow-sm">
|
| 46 |
<i data-lucide="bar-chart-3" class="h-4 w-4 mr-2"></i> Reports
|
| 47 |
</a>
|
|
@@ -272,9 +276,12 @@ else
|
|
| 272 |
private string currentUserName = "User";
|
| 273 |
private string currentUserRole = "Member";
|
| 274 |
private List<DateTime> weekDates = new();
|
|
|
|
| 275 |
|
| 276 |
protected override async Task OnInitializedAsync()
|
| 277 |
{
|
|
|
|
|
|
|
| 278 |
await LoadDashboardData();
|
| 279 |
}
|
| 280 |
|
|
|
|
| 13 |
@inject ApiClientService ApiClient
|
| 14 |
@inject IJSRuntime JS
|
| 15 |
@inject AuthenticationStateProvider AuthStateProvider
|
| 16 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 17 |
@inject NavigationManager Navigation
|
| 18 |
|
| 19 |
<PageTitle>Dashboard</PageTitle>
|
|
|
|
| 40 |
<p class="text-slate-500 mt-1">Here's what's happening across your workspace today.</p>
|
| 41 |
</div>
|
| 42 |
<div class="flex gap-3">
|
| 43 |
+
@if (canCreateTask)
|
| 44 |
+
{
|
| 45 |
+
<a href="/tasks" class="inline-flex items-center rounded-xl bg-violet-600 text-white px-4 py-2.5 text-sm font-semibold hover:bg-violet-700 transition-colors shadow-sm">
|
| 46 |
+
<i data-lucide="plus" class="h-4 w-4 mr-2"></i> New Task
|
| 47 |
+
</a>
|
| 48 |
+
}
|
| 49 |
<a href="/reports/tasks" class="inline-flex items-center rounded-xl bg-violet-600 text-white px-4 py-2.5 text-sm font-semibold hover:bg-violet-700 transition-colors shadow-sm">
|
| 50 |
<i data-lucide="bar-chart-3" class="h-4 w-4 mr-2"></i> Reports
|
| 51 |
</a>
|
|
|
|
| 276 |
private string currentUserName = "User";
|
| 277 |
private string currentUserRole = "Member";
|
| 278 |
private List<DateTime> weekDates = new();
|
| 279 |
+
private bool canCreateTask = false;
|
| 280 |
|
| 281 |
protected override async Task OnInitializedAsync()
|
| 282 |
{
|
| 283 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 284 |
+
canCreateTask = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Create");
|
| 285 |
await LoadDashboardData();
|
| 286 |
}
|
| 287 |
|
TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor
CHANGED
|
@@ -28,10 +28,13 @@
|
|
| 28 |
<p class="text-slate-500 mt-1">@(ProjectId > 0 ? $"Project #{ProjectId} — " : "Global View — ")Drag tasks between columns.</p>
|
| 29 |
</div>
|
| 30 |
</div>
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
|
| 37 |
@if (isLoading)
|
|
@@ -169,6 +172,7 @@
|
|
| 169 |
private bool isLoading = true;
|
| 170 |
private bool isAdmin = false;
|
| 171 |
private bool isManager = false;
|
|
|
|
| 172 |
private long currentUserId = 0;
|
| 173 |
private bool showTaskModal = false;
|
| 174 |
private bool isSaving = false;
|
|
@@ -223,6 +227,11 @@
|
|
| 223 |
|
| 224 |
protected override async Task OnInitializedAsync()
|
| 225 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
await LoadTasks();
|
| 227 |
}
|
| 228 |
|
|
|
|
| 28 |
<p class="text-slate-500 mt-1">@(ProjectId > 0 ? $"Project #{ProjectId} — " : "Global View — ")Drag tasks between columns.</p>
|
| 29 |
</div>
|
| 30 |
</div>
|
| 31 |
+
@if (canCreateTask)
|
| 32 |
+
{
|
| 33 |
+
<button @onclick="OpenCreateTaskModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 34 |
+
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
| 35 |
+
New Task
|
| 36 |
+
</button>
|
| 37 |
+
}
|
| 38 |
</div>
|
| 39 |
|
| 40 |
@if (isLoading)
|
|
|
|
| 172 |
private bool isLoading = true;
|
| 173 |
private bool isAdmin = false;
|
| 174 |
private bool isManager = false;
|
| 175 |
+
private bool canCreateTask = false;
|
| 176 |
private long currentUserId = 0;
|
| 177 |
private bool showTaskModal = false;
|
| 178 |
private bool isSaving = false;
|
|
|
|
| 227 |
|
| 228 |
protected override async Task OnInitializedAsync()
|
| 229 |
{
|
| 230 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 231 |
+
var user = authState.User;
|
| 232 |
+
isAdmin = user.IsInRole("Admin");
|
| 233 |
+
isManager = user.IsInRole("Manager");
|
| 234 |
+
canCreateTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Create");
|
| 235 |
await LoadTasks();
|
| 236 |
}
|
| 237 |
|
TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor
CHANGED
|
@@ -8,6 +8,8 @@
|
|
| 8 |
@inject ApiClientService ApiClient
|
| 9 |
@inject IJSRuntime JS
|
| 10 |
@inject NavigationManager Navigation
|
|
|
|
|
|
|
| 11 |
|
| 12 |
<PageTitle>Project Assignment</PageTitle>
|
| 13 |
|
|
@@ -186,11 +188,14 @@
|
|
| 186 |
</p>
|
| 187 |
}
|
| 188 |
</div>
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
<
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
| 194 |
</div>
|
| 195 |
}
|
| 196 |
</div>
|
|
@@ -210,6 +215,7 @@
|
|
| 210 |
private bool isLoading = true;
|
| 211 |
private bool isLoadingMembers = false;
|
| 212 |
private bool isSaving = false;
|
|
|
|
| 213 |
private bool showConfirmDialog = false;
|
| 214 |
private string? statusMessage;
|
| 215 |
private string? successMessage;
|
|
@@ -231,6 +237,8 @@
|
|
| 231 |
|
| 232 |
protected override async Task OnInitializedAsync()
|
| 233 |
{
|
|
|
|
|
|
|
| 234 |
var client = ApiClient.CreateClient();
|
| 235 |
try
|
| 236 |
{
|
|
|
|
| 8 |
@inject ApiClientService ApiClient
|
| 9 |
@inject IJSRuntime JS
|
| 10 |
@inject NavigationManager Navigation
|
| 11 |
+
@inject AuthenticationStateProvider AuthStateProvider
|
| 12 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 13 |
|
| 14 |
<PageTitle>Project Assignment</PageTitle>
|
| 15 |
|
|
|
|
| 188 |
</p>
|
| 189 |
}
|
| 190 |
</div>
|
| 191 |
+
@if (canSaveAssignments)
|
| 192 |
+
{
|
| 193 |
+
<button @onclick="RequestSaveConfirmation" disabled="@(isSaving || isLoadingMembers)"
|
| 194 |
+
class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
|
| 195 |
+
<i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
|
| 196 |
+
@(isSaving ? "Saving changes..." : "Save Assignment Changes")
|
| 197 |
+
</button>
|
| 198 |
+
}
|
| 199 |
</div>
|
| 200 |
}
|
| 201 |
</div>
|
|
|
|
| 215 |
private bool isLoading = true;
|
| 216 |
private bool isLoadingMembers = false;
|
| 217 |
private bool isSaving = false;
|
| 218 |
+
private bool canSaveAssignments = false;
|
| 219 |
private bool showConfirmDialog = false;
|
| 220 |
private string? statusMessage;
|
| 221 |
private string? successMessage;
|
|
|
|
| 237 |
|
| 238 |
protected override async Task OnInitializedAsync()
|
| 239 |
{
|
| 240 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 241 |
+
canSaveAssignments = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Projects_Update");
|
| 242 |
var client = ApiClient.CreateClient();
|
| 243 |
try
|
| 244 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/Projects.razor
CHANGED
|
@@ -22,7 +22,7 @@
|
|
| 22 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Projects</h2>
|
| 23 |
<p class="text-slate-500 mt-1">@(isAdmin ? "Manage all projects." : "Your assigned projects.")</p>
|
| 24 |
</div>
|
| 25 |
-
@if (
|
| 26 |
{
|
| 27 |
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 28 |
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
|
@@ -90,17 +90,26 @@
|
|
| 90 |
<button @onclick="() => NavigateToKanban(project.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-violet-50 hover:text-violet-600 transition-all" title="View Board">
|
| 91 |
<i data-lucide="kanban" class="h-4 w-4"></i>
|
| 92 |
</button>
|
| 93 |
-
@if (
|
| 94 |
{
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
<
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
}
|
| 105 |
</div>
|
| 106 |
</td>
|
|
@@ -109,7 +118,7 @@
|
|
| 109 |
}
|
| 110 |
else
|
| 111 |
{
|
| 112 |
-
<EmptyState ColSpan="5" Icon="folder-kanban" Title="No projects yet" Subtitle="@(
|
| 113 |
}
|
| 114 |
</tbody>
|
| 115 |
</table>
|
|
@@ -293,6 +302,10 @@
|
|
| 293 |
@code {
|
| 294 |
private bool isLoading = true;
|
| 295 |
private bool isAdmin = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
private long currentUserId = 0;
|
| 297 |
private bool showModal = false;
|
| 298 |
private bool isEditing = false;
|
|
@@ -377,6 +390,7 @@
|
|
| 377 |
isAdmin = user.IsInRole("Admin");
|
| 378 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 379 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
|
|
|
| 380 |
await LoadProjects();
|
| 381 |
}
|
| 382 |
|
|
@@ -411,6 +425,14 @@
|
|
| 411 |
private void HandlePageChanged(int page) { currentPage = page; }
|
| 412 |
private void HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; }
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
private void ClearValidation()
|
| 415 |
{
|
| 416 |
isNameError = false;
|
|
|
|
| 22 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Projects</h2>
|
| 23 |
<p class="text-slate-500 mt-1">@(isAdmin ? "Manage all projects." : "Your assigned projects.")</p>
|
| 24 |
</div>
|
| 25 |
+
@if (canCreateProject)
|
| 26 |
{
|
| 27 |
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 28 |
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
|
|
|
| 90 |
<button @onclick="() => NavigateToKanban(project.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-violet-50 hover:text-violet-600 transition-all" title="View Board">
|
| 91 |
<i data-lucide="kanban" class="h-4 w-4"></i>
|
| 92 |
</button>
|
| 93 |
+
@if (canAssignProject || canEditProject || canDeleteProject)
|
| 94 |
{
|
| 95 |
+
@if (canAssignProject)
|
| 96 |
+
{
|
| 97 |
+
<button @onclick="() => OpenAssignModal(project)" class="rounded-lg p-1.5 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600 transition-all" title="Assign Members">
|
| 98 |
+
<i data-lucide="user-plus" class="h-4 w-4"></i>
|
| 99 |
+
</button>
|
| 100 |
+
}
|
| 101 |
+
@if (canEditProject)
|
| 102 |
+
{
|
| 103 |
+
<button @onclick="() => OpenEditModal(project)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
|
| 104 |
+
<i data-lucide="pencil" class="h-4 w-4"></i>
|
| 105 |
+
</button>
|
| 106 |
+
}
|
| 107 |
+
@if (canDeleteProject)
|
| 108 |
+
{
|
| 109 |
+
<button @onclick="() => RequestDeleteConfirmation(project.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
|
| 110 |
+
<i data-lucide="trash-2" class="h-4 w-4"></i>
|
| 111 |
+
</button>
|
| 112 |
+
}
|
| 113 |
}
|
| 114 |
</div>
|
| 115 |
</td>
|
|
|
|
| 118 |
}
|
| 119 |
else
|
| 120 |
{
|
| 121 |
+
<EmptyState ColSpan="5" Icon="folder-kanban" Title="No projects yet" Subtitle="@(canCreateProject ? "Create your first project to get started." : "You have not been assigned to any projects.")" />
|
| 122 |
}
|
| 123 |
</tbody>
|
| 124 |
</table>
|
|
|
|
| 302 |
@code {
|
| 303 |
private bool isLoading = true;
|
| 304 |
private bool isAdmin = false;
|
| 305 |
+
private bool canCreateProject = false;
|
| 306 |
+
private bool canEditProject = false;
|
| 307 |
+
private bool canDeleteProject = false;
|
| 308 |
+
private bool canAssignProject = false;
|
| 309 |
private long currentUserId = 0;
|
| 310 |
private bool showModal = false;
|
| 311 |
private bool isEditing = false;
|
|
|
|
| 390 |
isAdmin = user.IsInRole("Admin");
|
| 391 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 392 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
| 393 |
+
await LoadProjectAccessAsync(user);
|
| 394 |
await LoadProjects();
|
| 395 |
}
|
| 396 |
|
|
|
|
| 425 |
private void HandlePageChanged(int page) { currentPage = page; }
|
| 426 |
private void HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; }
|
| 427 |
|
| 428 |
+
private async Task LoadProjectAccessAsync(System.Security.Claims.ClaimsPrincipal user)
|
| 429 |
+
{
|
| 430 |
+
canCreateProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Create");
|
| 431 |
+
canEditProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Update");
|
| 432 |
+
canDeleteProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Delete");
|
| 433 |
+
canAssignProject = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Projects_Update");
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
private void ClearValidation()
|
| 437 |
{
|
| 438 |
isNameError = false;
|
TaskTrackingSystem.WebApp/Components/Pages/Roles.razor
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
@page "/roles"
|
| 2 |
@using Microsoft.AspNetCore.Authorization
|
|
|
|
| 3 |
@using TaskTrackingSystem.Shared.Models.Menu
|
| 4 |
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 5 |
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
| 6 |
@inject ApiClientService ApiClient
|
| 7 |
@inject IJSRuntime JS
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
|
|
@@ -19,10 +22,13 @@
|
|
| 19 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Roles & Access</h2>
|
| 20 |
<p class="text-slate-500 mt-1">Manage roles, assign access and permissions, and control visibility.</p>
|
| 21 |
</div>
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
</div>
|
| 27 |
|
| 28 |
@if (isLoading)
|
|
@@ -77,18 +83,27 @@
|
|
| 77 |
<td class="px-6 py-4 text-sm text-slate-400">@role.CreatedAt.ToString("dd-MMM-yyyy")</td>
|
| 78 |
<td class="px-6 py-4 text-right">
|
| 79 |
<div class="flex items-center justify-end space-x-2">
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
<
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
</td>
|
| 94 |
</tr>
|
|
@@ -256,13 +271,13 @@
|
|
| 256 |
<div class="flex flex-wrap gap-2 pl-7 mt-1">
|
| 257 |
@foreach (var action in child.Permissions)
|
| 258 |
{
|
| 259 |
-
var isActionChecked = formSelectedAccessCodes.Contains(action.
|
| 260 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
|
| 261 |
@(isActionChecked
|
| 262 |
? "bg-violet-50 border-violet-200 text-violet-700"
|
| 263 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 264 |
<input type="checkbox" checked="@isActionChecked"
|
| 265 |
-
@onchange="(e) => TogglePermissionSelection(action.
|
| 266 |
class="h-3 w-3 rounded accent-violet-600" />
|
| 267 |
<span class="text-xs">@action.ActionName</span>
|
| 268 |
</label>
|
|
@@ -279,13 +294,13 @@
|
|
| 279 |
<div class="px-5 py-2.5 bg-white flex flex-wrap gap-2">
|
| 280 |
@foreach (var action in parent.Permissions)
|
| 281 |
{
|
| 282 |
-
var isActionChecked = formSelectedAccessCodes.Contains(action.
|
| 283 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border cursor-pointer select-none transition-all
|
| 284 |
@(isActionChecked
|
| 285 |
? "bg-violet-50 border-violet-200 text-violet-700 shadow-sm"
|
| 286 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 287 |
<input type="checkbox" checked="@isActionChecked"
|
| 288 |
-
@onchange="(e) => TogglePermissionSelection(action.
|
| 289 |
class="h-3.5 w-3.5 rounded accent-violet-600" />
|
| 290 |
<span class="text-xs font-medium">@action.ActionName</span>
|
| 291 |
</label>
|
|
@@ -420,13 +435,13 @@
|
|
| 420 |
<div class="flex flex-wrap gap-2 pl-7 mt-1.5">
|
| 421 |
@foreach (var action in child.Permissions)
|
| 422 |
{
|
| 423 |
-
var isActionChecked = selectedAccessCodes.Contains(action.
|
| 424 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
|
| 425 |
@(isActionChecked
|
| 426 |
? "bg-violet-50 border-violet-200 text-violet-700"
|
| 427 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 428 |
<input type="checkbox" checked="@isActionChecked"
|
| 429 |
-
@onchange="(e) => TogglePermissionSelection(action.
|
| 430 |
class="h-3 w-3 rounded accent-violet-600" />
|
| 431 |
<span class="text-xs">@action.ActionName</span>
|
| 432 |
</label>
|
|
@@ -443,13 +458,13 @@
|
|
| 443 |
<div class="px-6 py-3.5 bg-white flex flex-wrap gap-2.5">
|
| 444 |
@foreach (var action in parent.Permissions)
|
| 445 |
{
|
| 446 |
-
var isActionChecked = selectedAccessCodes.Contains(action.
|
| 447 |
<label class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border cursor-pointer select-none transition-all
|
| 448 |
@(isActionChecked
|
| 449 |
? "bg-violet-50 border-violet-300 text-violet-700 shadow-sm"
|
| 450 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 451 |
<input type="checkbox" checked="@isActionChecked"
|
| 452 |
-
@onchange="(e) => TogglePermissionSelection(action.
|
| 453 |
class="h-3.5 w-3.5 rounded accent-violet-600" />
|
| 454 |
<span class="text-xs font-medium">@action.ActionName</span>
|
| 455 |
</label>
|
|
@@ -520,6 +535,10 @@
|
|
| 520 |
private bool showAccessModal = false;
|
| 521 |
private bool isEditing = false;
|
| 522 |
private bool isSaving = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
private string? roleError;
|
| 524 |
private bool isRoleNameError = false;
|
| 525 |
private string roleNameErrorMsg = "";
|
|
@@ -597,6 +616,12 @@
|
|
| 597 |
|
| 598 |
protected override async Task OnInitializedAsync()
|
| 599 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
await LoadRoles();
|
| 601 |
}
|
| 602 |
|
|
@@ -700,7 +725,7 @@
|
|
| 700 |
// Select direct actions
|
| 701 |
foreach (var action in menu.Permissions)
|
| 702 |
{
|
| 703 |
-
targetSet.Add(action.
|
| 704 |
}
|
| 705 |
// Select child menus and their actions
|
| 706 |
var children = allMenuSource.Where(c => c.ParentCode == menu.MenuCode).ToList();
|
|
@@ -709,7 +734,7 @@
|
|
| 709 |
targetSet.Add(child.MenuCode);
|
| 710 |
foreach (var action in child.Permissions)
|
| 711 |
{
|
| 712 |
-
targetSet.Add(action.
|
| 713 |
}
|
| 714 |
}
|
| 715 |
}
|
|
@@ -719,7 +744,7 @@
|
|
| 719 |
// Unselect direct actions
|
| 720 |
foreach (var action in menu.Permissions)
|
| 721 |
{
|
| 722 |
-
targetSet.Remove(action.
|
| 723 |
}
|
| 724 |
// Unselect child menus and their actions
|
| 725 |
var children = allMenuSource.Where(c => c.ParentCode == menu.MenuCode).ToList();
|
|
@@ -728,7 +753,7 @@
|
|
| 728 |
targetSet.Remove(child.MenuCode);
|
| 729 |
foreach (var action in child.Permissions)
|
| 730 |
{
|
| 731 |
-
targetSet.Remove(action.
|
| 732 |
}
|
| 733 |
}
|
| 734 |
}
|
|
@@ -760,7 +785,7 @@
|
|
| 760 |
formSelectedAccessCodes.Add(menu.MenuCode);
|
| 761 |
foreach (var action in menu.Permissions)
|
| 762 |
{
|
| 763 |
-
formSelectedAccessCodes.Add(action.
|
| 764 |
}
|
| 765 |
}
|
| 766 |
}
|
|
@@ -951,7 +976,7 @@
|
|
| 951 |
selectedAccessCodes.Add(menu.MenuCode);
|
| 952 |
foreach (var action in menu.Permissions)
|
| 953 |
{
|
| 954 |
-
selectedAccessCodes.Add(action.
|
| 955 |
}
|
| 956 |
}
|
| 957 |
}
|
|
|
|
| 1 |
@page "/roles"
|
| 2 |
@using Microsoft.AspNetCore.Authorization
|
| 3 |
+
@using Microsoft.AspNetCore.Components.Authorization
|
| 4 |
@using TaskTrackingSystem.Shared.Models.Menu
|
| 5 |
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 6 |
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
| 7 |
@inject ApiClientService ApiClient
|
| 8 |
@inject IJSRuntime JS
|
| 9 |
+
@inject AuthenticationStateProvider AuthStateProvider
|
| 10 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 11 |
|
| 12 |
|
| 13 |
|
|
|
|
| 22 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Roles & Access</h2>
|
| 23 |
<p class="text-slate-500 mt-1">Manage roles, assign access and permissions, and control visibility.</p>
|
| 24 |
</div>
|
| 25 |
+
@if (canCreateRole)
|
| 26 |
+
{
|
| 27 |
+
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 28 |
+
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
| 29 |
+
New Role
|
| 30 |
+
</button>
|
| 31 |
+
}
|
| 32 |
</div>
|
| 33 |
|
| 34 |
@if (isLoading)
|
|
|
|
| 83 |
<td class="px-6 py-4 text-sm text-slate-400">@role.CreatedAt.ToString("dd-MMM-yyyy")</td>
|
| 84 |
<td class="px-6 py-4 text-right">
|
| 85 |
<div class="flex items-center justify-end space-x-2">
|
| 86 |
+
@if (canManageAccess)
|
| 87 |
+
{
|
| 88 |
+
<button @onclick="() => OpenAccessModal(role)"
|
| 89 |
+
class="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100 transition-all"
|
| 90 |
+
title="Manage Access">
|
| 91 |
+
<i data-lucide="shield-check" class="h-3.5 w-3.5"></i>
|
| 92 |
+
Access
|
| 93 |
+
</button>
|
| 94 |
+
}
|
| 95 |
+
@if (canEditRole)
|
| 96 |
+
{
|
| 97 |
+
<button @onclick="() => OpenEditModal(role)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
|
| 98 |
+
<i data-lucide="pencil" class="h-4 w-4"></i>
|
| 99 |
+
</button>
|
| 100 |
+
}
|
| 101 |
+
@if (canDeleteRole)
|
| 102 |
+
{
|
| 103 |
+
<button @onclick="() => RequestDeleteConfirmation(role.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
|
| 104 |
+
<i data-lucide="trash-2" class="h-4 w-4"></i>
|
| 105 |
+
</button>
|
| 106 |
+
}
|
| 107 |
</div>
|
| 108 |
</td>
|
| 109 |
</tr>
|
|
|
|
| 271 |
<div class="flex flex-wrap gap-2 pl-7 mt-1">
|
| 272 |
@foreach (var action in child.Permissions)
|
| 273 |
{
|
| 274 |
+
var isActionChecked = formSelectedAccessCodes.Contains(action.PermissionCode);
|
| 275 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
|
| 276 |
@(isActionChecked
|
| 277 |
? "bg-violet-50 border-violet-200 text-violet-700"
|
| 278 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 279 |
<input type="checkbox" checked="@isActionChecked"
|
| 280 |
+
@onchange="(e) => TogglePermissionSelection(action.PermissionCode, (bool)(e.Value ?? false), isForm: true)"
|
| 281 |
class="h-3 w-3 rounded accent-violet-600" />
|
| 282 |
<span class="text-xs">@action.ActionName</span>
|
| 283 |
</label>
|
|
|
|
| 294 |
<div class="px-5 py-2.5 bg-white flex flex-wrap gap-2">
|
| 295 |
@foreach (var action in parent.Permissions)
|
| 296 |
{
|
| 297 |
+
var isActionChecked = formSelectedAccessCodes.Contains(action.PermissionCode);
|
| 298 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border cursor-pointer select-none transition-all
|
| 299 |
@(isActionChecked
|
| 300 |
? "bg-violet-50 border-violet-200 text-violet-700 shadow-sm"
|
| 301 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 302 |
<input type="checkbox" checked="@isActionChecked"
|
| 303 |
+
@onchange="(e) => TogglePermissionSelection(action.PermissionCode, (bool)(e.Value ?? false), isForm: true)"
|
| 304 |
class="h-3.5 w-3.5 rounded accent-violet-600" />
|
| 305 |
<span class="text-xs font-medium">@action.ActionName</span>
|
| 306 |
</label>
|
|
|
|
| 435 |
<div class="flex flex-wrap gap-2 pl-7 mt-1.5">
|
| 436 |
@foreach (var action in child.Permissions)
|
| 437 |
{
|
| 438 |
+
var isActionChecked = selectedAccessCodes.Contains(action.PermissionCode);
|
| 439 |
<label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
|
| 440 |
@(isActionChecked
|
| 441 |
? "bg-violet-50 border-violet-200 text-violet-700"
|
| 442 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 443 |
<input type="checkbox" checked="@isActionChecked"
|
| 444 |
+
@onchange="(e) => TogglePermissionSelection(action.PermissionCode, (bool)(e.Value ?? false), isForm: false)"
|
| 445 |
class="h-3 w-3 rounded accent-violet-600" />
|
| 446 |
<span class="text-xs">@action.ActionName</span>
|
| 447 |
</label>
|
|
|
|
| 458 |
<div class="px-6 py-3.5 bg-white flex flex-wrap gap-2.5">
|
| 459 |
@foreach (var action in parent.Permissions)
|
| 460 |
{
|
| 461 |
+
var isActionChecked = selectedAccessCodes.Contains(action.PermissionCode);
|
| 462 |
<label class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border cursor-pointer select-none transition-all
|
| 463 |
@(isActionChecked
|
| 464 |
? "bg-violet-50 border-violet-300 text-violet-700 shadow-sm"
|
| 465 |
: "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
|
| 466 |
<input type="checkbox" checked="@isActionChecked"
|
| 467 |
+
@onchange="(e) => TogglePermissionSelection(action.PermissionCode, (bool)(e.Value ?? false), isForm: false)"
|
| 468 |
class="h-3.5 w-3.5 rounded accent-violet-600" />
|
| 469 |
<span class="text-xs font-medium">@action.ActionName</span>
|
| 470 |
</label>
|
|
|
|
| 535 |
private bool showAccessModal = false;
|
| 536 |
private bool isEditing = false;
|
| 537 |
private bool isSaving = false;
|
| 538 |
+
private bool canCreateRole = false;
|
| 539 |
+
private bool canEditRole = false;
|
| 540 |
+
private bool canDeleteRole = false;
|
| 541 |
+
private bool canManageAccess = false;
|
| 542 |
private string? roleError;
|
| 543 |
private bool isRoleNameError = false;
|
| 544 |
private string roleNameErrorMsg = "";
|
|
|
|
| 616 |
|
| 617 |
protected override async Task OnInitializedAsync()
|
| 618 |
{
|
| 619 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 620 |
+
var user = authState.User;
|
| 621 |
+
canCreateRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Create");
|
| 622 |
+
canEditRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Update");
|
| 623 |
+
canDeleteRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Delete");
|
| 624 |
+
canManageAccess = canEditRole;
|
| 625 |
await LoadRoles();
|
| 626 |
}
|
| 627 |
|
|
|
|
| 725 |
// Select direct actions
|
| 726 |
foreach (var action in menu.Permissions)
|
| 727 |
{
|
| 728 |
+
targetSet.Add(action.PermissionCode);
|
| 729 |
}
|
| 730 |
// Select child menus and their actions
|
| 731 |
var children = allMenuSource.Where(c => c.ParentCode == menu.MenuCode).ToList();
|
|
|
|
| 734 |
targetSet.Add(child.MenuCode);
|
| 735 |
foreach (var action in child.Permissions)
|
| 736 |
{
|
| 737 |
+
targetSet.Add(action.PermissionCode);
|
| 738 |
}
|
| 739 |
}
|
| 740 |
}
|
|
|
|
| 744 |
// Unselect direct actions
|
| 745 |
foreach (var action in menu.Permissions)
|
| 746 |
{
|
| 747 |
+
targetSet.Remove(action.PermissionCode);
|
| 748 |
}
|
| 749 |
// Unselect child menus and their actions
|
| 750 |
var children = allMenuSource.Where(c => c.ParentCode == menu.MenuCode).ToList();
|
|
|
|
| 753 |
targetSet.Remove(child.MenuCode);
|
| 754 |
foreach (var action in child.Permissions)
|
| 755 |
{
|
| 756 |
+
targetSet.Remove(action.PermissionCode);
|
| 757 |
}
|
| 758 |
}
|
| 759 |
}
|
|
|
|
| 785 |
formSelectedAccessCodes.Add(menu.MenuCode);
|
| 786 |
foreach (var action in menu.Permissions)
|
| 787 |
{
|
| 788 |
+
formSelectedAccessCodes.Add(action.PermissionCode);
|
| 789 |
}
|
| 790 |
}
|
| 791 |
}
|
|
|
|
| 976 |
selectedAccessCodes.Add(menu.MenuCode);
|
| 977 |
foreach (var action in menu.Permissions)
|
| 978 |
{
|
| 979 |
+
selectedAccessCodes.Add(action.PermissionCode);
|
| 980 |
}
|
| 981 |
}
|
| 982 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor
CHANGED
|
@@ -9,6 +9,8 @@
|
|
| 9 |
@inject ApiClientService ApiClient
|
| 10 |
@inject IJSRuntime JS
|
| 11 |
@inject NavigationManager Navigation
|
|
|
|
|
|
|
| 12 |
|
| 13 |
<PageTitle>Task Assignment</PageTitle>
|
| 14 |
|
|
@@ -221,11 +223,14 @@
|
|
| 221 |
</p>
|
| 222 |
}
|
| 223 |
</div>
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
<
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
| 229 |
</div>
|
| 230 |
}
|
| 231 |
</div>
|
|
@@ -244,6 +249,7 @@
|
|
| 244 |
@code {
|
| 245 |
private bool isLoading = true;
|
| 246 |
private bool isSaving = false;
|
|
|
|
| 247 |
private bool showConfirmDialog = false;
|
| 248 |
private string? statusMessage;
|
| 249 |
private string? successMessage;
|
|
@@ -268,6 +274,8 @@
|
|
| 268 |
|
| 269 |
protected override async Task OnInitializedAsync()
|
| 270 |
{
|
|
|
|
|
|
|
| 271 |
var client = ApiClient.CreateClient();
|
| 272 |
try
|
| 273 |
{
|
|
|
|
| 9 |
@inject ApiClientService ApiClient
|
| 10 |
@inject IJSRuntime JS
|
| 11 |
@inject NavigationManager Navigation
|
| 12 |
+
@inject AuthenticationStateProvider AuthStateProvider
|
| 13 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 14 |
|
| 15 |
<PageTitle>Task Assignment</PageTitle>
|
| 16 |
|
|
|
|
| 223 |
</p>
|
| 224 |
}
|
| 225 |
</div>
|
| 226 |
+
@if (canSaveAssignments)
|
| 227 |
+
{
|
| 228 |
+
<button @onclick="RequestSaveConfirmation" disabled="@isSaving"
|
| 229 |
+
class="w-full sm:w-auto inline-flex items-center justify-center rounded-lg bg-violet-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
|
| 230 |
+
<i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
|
| 231 |
+
@(isSaving ? "Saving..." : "Save Assignment")
|
| 232 |
+
</button>
|
| 233 |
+
}
|
| 234 |
</div>
|
| 235 |
}
|
| 236 |
</div>
|
|
|
|
| 249 |
@code {
|
| 250 |
private bool isLoading = true;
|
| 251 |
private bool isSaving = false;
|
| 252 |
+
private bool canSaveAssignments = false;
|
| 253 |
private bool showConfirmDialog = false;
|
| 254 |
private string? statusMessage;
|
| 255 |
private string? successMessage;
|
|
|
|
| 274 |
|
| 275 |
protected override async Task OnInitializedAsync()
|
| 276 |
{
|
| 277 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 278 |
+
canSaveAssignments = authState.User.IsInRole("Admin") || await MenuAuthorization.HasAccessCodeAsync(authState.User, "Tasks_Update");
|
| 279 |
var client = ApiClient.CreateClient();
|
| 280 |
try
|
| 281 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
@inject ApiClientService ApiClient
|
| 8 |
@inject IJSRuntime JS
|
| 9 |
@inject AuthenticationStateProvider AuthStateProvider
|
|
|
|
| 10 |
@inject NavigationManager Navigation
|
| 11 |
|
| 12 |
<PageTitle>Tasks</PageTitle>
|
|
@@ -37,10 +38,13 @@
|
|
| 37 |
</button>
|
| 38 |
</div>
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
</div>
|
| 45 |
</div>
|
| 46 |
|
|
@@ -108,9 +112,12 @@
|
|
| 108 |
<h3 class="font-bold text-sm text-slate-800">@proj.Name</h3>
|
| 109 |
<span class="px-2.5 py-0.5 rounded-full bg-slate-200/80 text-[10px] text-slate-500 font-semibold font-sans">@projTasks.Count tasks</span>
|
| 110 |
</div>
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
| 114 |
</div>
|
| 115 |
|
| 116 |
<!-- Tasks List (Vertical Backlog rows) -->
|
|
@@ -120,7 +127,7 @@
|
|
| 120 |
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-4 hover:bg-slate-50/40 transition-colors gap-3">
|
| 121 |
<!-- Left section -->
|
| 122 |
<div class="flex items-start space-x-3 flex-1 min-w-0">
|
| 123 |
-
<input type="checkbox" checked="@(task.StatusId == 3)" @onchange="() => ToggleTaskStatus(task)" class="h-4 w-4 rounded border-slate-300 text-violet-600 accent-violet-600 focus:ring-violet-500 mt-1 cursor-pointer shrink-0" />
|
| 124 |
<div class="min-w-0">
|
| 125 |
<div class="flex items-center space-x-2">
|
| 126 |
<span class="text-[10px] font-bold text-slate-400 font-mono tracking-wider font-semibold">TASK-#@task.Id</span>
|
|
@@ -146,12 +153,18 @@
|
|
| 146 |
</span>
|
| 147 |
|
| 148 |
<div class="flex items-center space-x-1">
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
<
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</div>
|
| 156 |
</div>
|
| 157 |
</div>
|
|
@@ -293,12 +306,18 @@
|
|
| 293 |
</td>
|
| 294 |
<td class="px-6 py-4 text-right">
|
| 295 |
<div class="flex items-center justify-end space-x-2">
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
<
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
</div>
|
| 303 |
</td>
|
| 304 |
</tr>
|
|
@@ -448,6 +467,10 @@
|
|
| 448 |
private bool isLoading = true;
|
| 449 |
private bool isAdmin = false;
|
| 450 |
private bool isManager = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
private long currentUserId = 0;
|
| 452 |
private bool showModal = false;
|
| 453 |
private bool isEditing = false;
|
|
@@ -645,6 +668,7 @@
|
|
| 645 |
isManager = user.IsInRole("Manager");
|
| 646 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 647 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
|
|
|
| 648 |
|
| 649 |
var client = ApiClient.CreateClient();
|
| 650 |
try
|
|
@@ -708,6 +732,14 @@
|
|
| 708 |
currentPage = 1;
|
| 709 |
}
|
| 710 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
private void OpenCreateModal()
|
| 712 |
{
|
| 713 |
isEditing = false;
|
|
|
|
| 7 |
@inject ApiClientService ApiClient
|
| 8 |
@inject IJSRuntime JS
|
| 9 |
@inject AuthenticationStateProvider AuthStateProvider
|
| 10 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 11 |
@inject NavigationManager Navigation
|
| 12 |
|
| 13 |
<PageTitle>Tasks</PageTitle>
|
|
|
|
| 38 |
</button>
|
| 39 |
</div>
|
| 40 |
|
| 41 |
+
@if (canCreateTask)
|
| 42 |
+
{
|
| 43 |
+
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 44 |
+
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
| 45 |
+
New Task
|
| 46 |
+
</button>
|
| 47 |
+
}
|
| 48 |
</div>
|
| 49 |
</div>
|
| 50 |
|
|
|
|
| 112 |
<h3 class="font-bold text-sm text-slate-800">@proj.Name</h3>
|
| 113 |
<span class="px-2.5 py-0.5 rounded-full bg-slate-200/80 text-[10px] text-slate-500 font-semibold font-sans">@projTasks.Count tasks</span>
|
| 114 |
</div>
|
| 115 |
+
@if (canCreateTask)
|
| 116 |
+
{
|
| 117 |
+
<button @onclick="() => OpenCreateModalForProject(proj.Id)" class="inline-flex items-center text-xs font-semibold text-emerald-600 hover:text-emerald-800 transition-colors">
|
| 118 |
+
<i data-lucide="plus" class="h-3 w-3 mr-1"></i> Add Task
|
| 119 |
+
</button>
|
| 120 |
+
}
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<!-- Tasks List (Vertical Backlog rows) -->
|
|
|
|
| 127 |
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-4 hover:bg-slate-50/40 transition-colors gap-3">
|
| 128 |
<!-- Left section -->
|
| 129 |
<div class="flex items-start space-x-3 flex-1 min-w-0">
|
| 130 |
+
<input type="checkbox" checked="@(task.StatusId == 3)" @onchange="() => ToggleTaskStatus(task)" disabled="@(!canUpdateTask)" class="h-4 w-4 rounded border-slate-300 text-violet-600 accent-violet-600 focus:ring-violet-500 mt-1 cursor-pointer shrink-0 disabled:cursor-not-allowed disabled:opacity-50" />
|
| 131 |
<div class="min-w-0">
|
| 132 |
<div class="flex items-center space-x-2">
|
| 133 |
<span class="text-[10px] font-bold text-slate-400 font-mono tracking-wider font-semibold">TASK-#@task.Id</span>
|
|
|
|
| 153 |
</span>
|
| 154 |
|
| 155 |
<div class="flex items-center space-x-1">
|
| 156 |
+
@if (canEditTask)
|
| 157 |
+
{
|
| 158 |
+
<button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
|
| 159 |
+
<i data-lucide="pencil" class="h-3.5 w-3.5"></i>
|
| 160 |
+
</button>
|
| 161 |
+
}
|
| 162 |
+
@if (canDeleteTask)
|
| 163 |
+
{
|
| 164 |
+
<button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
|
| 165 |
+
<i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
|
| 166 |
+
</button>
|
| 167 |
+
}
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
</div>
|
|
|
|
| 306 |
</td>
|
| 307 |
<td class="px-6 py-4 text-right">
|
| 308 |
<div class="flex items-center justify-end space-x-2">
|
| 309 |
+
@if (canEditTask)
|
| 310 |
+
{
|
| 311 |
+
<button @onclick="() => OpenEditModal(task)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
|
| 312 |
+
<i data-lucide="pencil" class="h-4 w-4"></i>
|
| 313 |
+
</button>
|
| 314 |
+
}
|
| 315 |
+
@if (canDeleteTask)
|
| 316 |
+
{
|
| 317 |
+
<button @onclick="() => RequestDeleteConfirmation(task.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
|
| 318 |
+
<i data-lucide="trash-2" class="h-4 w-4"></i>
|
| 319 |
+
</button>
|
| 320 |
+
}
|
| 321 |
</div>
|
| 322 |
</td>
|
| 323 |
</tr>
|
|
|
|
| 467 |
private bool isLoading = true;
|
| 468 |
private bool isAdmin = false;
|
| 469 |
private bool isManager = false;
|
| 470 |
+
private bool canCreateTask = false;
|
| 471 |
+
private bool canEditTask = false;
|
| 472 |
+
private bool canDeleteTask = false;
|
| 473 |
+
private bool canUpdateTask = false;
|
| 474 |
private long currentUserId = 0;
|
| 475 |
private bool showModal = false;
|
| 476 |
private bool isEditing = false;
|
|
|
|
| 668 |
isManager = user.IsInRole("Manager");
|
| 669 |
var idStr = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
| 670 |
if (idStr != null) long.TryParse(idStr, out currentUserId);
|
| 671 |
+
await LoadTaskAccessAsync(user);
|
| 672 |
|
| 673 |
var client = ApiClient.CreateClient();
|
| 674 |
try
|
|
|
|
| 732 |
currentPage = 1;
|
| 733 |
}
|
| 734 |
|
| 735 |
+
private async Task LoadTaskAccessAsync(System.Security.Claims.ClaimsPrincipal user)
|
| 736 |
+
{
|
| 737 |
+
canCreateTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Create");
|
| 738 |
+
canEditTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Update");
|
| 739 |
+
canDeleteTask = isAdmin || await MenuAuthorization.HasAccessCodeAsync(user, "Tasks_Delete");
|
| 740 |
+
canUpdateTask = canEditTask;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
private void OpenCreateModal()
|
| 744 |
{
|
| 745 |
isEditing = false;
|
TaskTrackingSystem.WebApp/Components/Pages/Users.razor
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
@using Microsoft.AspNetCore.Components.Authorization
|
| 8 |
@inject AuthenticationStateProvider AuthStateProvider
|
|
|
|
| 9 |
|
| 10 |
<PageTitle>Users</PageTitle>
|
| 11 |
|
|
@@ -18,10 +19,13 @@
|
|
| 18 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Users</h2>
|
| 19 |
<p class="text-slate-500 mt-1">Manage system users and their roles.</p>
|
| 20 |
</div>
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
</div>
|
| 26 |
|
| 27 |
<!-- Search and Filters -->
|
|
@@ -111,12 +115,18 @@
|
|
| 111 |
</td>
|
| 112 |
<td class="px-6 py-4 text-right">
|
| 113 |
<div class="flex items-center justify-end space-x-2">
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
<
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</div>
|
| 121 |
</td>
|
| 122 |
</tr>
|
|
@@ -264,6 +274,9 @@
|
|
| 264 |
private bool showModal = false;
|
| 265 |
private bool isEditing = false;
|
| 266 |
private bool isSaving = false;
|
|
|
|
|
|
|
|
|
|
| 267 |
private string? errorMessage;
|
| 268 |
|
| 269 |
// Per-field validation errors
|
|
@@ -368,6 +381,12 @@
|
|
| 368 |
|
| 369 |
protected override async Task OnInitializedAsync()
|
| 370 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
var client = ApiClient.CreateClient();
|
| 372 |
try
|
| 373 |
{
|
|
|
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
@using Microsoft.AspNetCore.Components.Authorization
|
| 8 |
@inject AuthenticationStateProvider AuthStateProvider
|
| 9 |
+
@inject MenuAuthorizationService MenuAuthorization
|
| 10 |
|
| 11 |
<PageTitle>Users</PageTitle>
|
| 12 |
|
|
|
|
| 19 |
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Users</h2>
|
| 20 |
<p class="text-slate-500 mt-1">Manage system users and their roles.</p>
|
| 21 |
</div>
|
| 22 |
+
@if (canCreateUser)
|
| 23 |
+
{
|
| 24 |
+
<button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
|
| 25 |
+
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
|
| 26 |
+
New User
|
| 27 |
+
</button>
|
| 28 |
+
}
|
| 29 |
</div>
|
| 30 |
|
| 31 |
<!-- Search and Filters -->
|
|
|
|
| 115 |
</td>
|
| 116 |
<td class="px-6 py-4 text-right">
|
| 117 |
<div class="flex items-center justify-end space-x-2">
|
| 118 |
+
@if (canEditUser)
|
| 119 |
+
{
|
| 120 |
+
<button @onclick="() => OpenEditModal(user)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
|
| 121 |
+
<i data-lucide="pencil" class="h-4 w-4"></i>
|
| 122 |
+
</button>
|
| 123 |
+
}
|
| 124 |
+
@if (canDeleteUser)
|
| 125 |
+
{
|
| 126 |
+
<button @onclick="() => RequestDeleteConfirmation(user.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="Delete">
|
| 127 |
+
<i data-lucide="trash-2" class="h-4 w-4"></i>
|
| 128 |
+
</button>
|
| 129 |
+
}
|
| 130 |
</div>
|
| 131 |
</td>
|
| 132 |
</tr>
|
|
|
|
| 274 |
private bool showModal = false;
|
| 275 |
private bool isEditing = false;
|
| 276 |
private bool isSaving = false;
|
| 277 |
+
private bool canCreateUser = false;
|
| 278 |
+
private bool canEditUser = false;
|
| 279 |
+
private bool canDeleteUser = false;
|
| 280 |
private string? errorMessage;
|
| 281 |
|
| 282 |
// Per-field validation errors
|
|
|
|
| 381 |
|
| 382 |
protected override async Task OnInitializedAsync()
|
| 383 |
{
|
| 384 |
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 385 |
+
var user = authState.User;
|
| 386 |
+
canCreateUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Create");
|
| 387 |
+
canEditUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Update");
|
| 388 |
+
canDeleteUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Delete");
|
| 389 |
+
|
| 390 |
var client = ApiClient.CreateClient();
|
| 391 |
try
|
| 392 |
{
|
TaskTrackingSystem.WebApp/MenuAuthorizationService.cs
CHANGED
|
@@ -17,7 +17,8 @@ public class MenuAuthorizationService
|
|
| 17 |
|
| 18 |
private readonly ApiClientService _apiClient;
|
| 19 |
private readonly UserSessionState _sessionState;
|
| 20 |
-
private Task<List<MenuDto>>?
|
|
|
|
| 21 |
|
| 22 |
public MenuAuthorizationService(ApiClientService apiClient, UserSessionState sessionState)
|
| 23 |
{
|
|
@@ -65,15 +66,15 @@ public class MenuAuthorizationService
|
|
| 65 |
|
| 66 |
var roleKey = GetRoleCacheKey(user);
|
| 67 |
|
| 68 |
-
if (_sessionState.
|
| 69 |
{
|
| 70 |
-
return IsRouteAllowed(_sessionState.
|
| 71 |
}
|
| 72 |
|
| 73 |
return false;
|
| 74 |
}
|
| 75 |
|
| 76 |
-
public bool IsRouteAllowed(IReadOnlyList<MenuDto>
|
| 77 |
{
|
| 78 |
var firstSegment = GetFirstSegment(relativePath);
|
| 79 |
|
|
@@ -85,10 +86,10 @@ public class MenuAuthorizationService
|
|
| 85 |
if (firstSegment.Equals("dashboard", StringComparison.OrdinalIgnoreCase) ||
|
| 86 |
firstSegment.Equals("home", StringComparison.OrdinalIgnoreCase))
|
| 87 |
{
|
| 88 |
-
return MenuCollectionMatchesSegment(
|
| 89 |
}
|
| 90 |
|
| 91 |
-
return MenuCollectionMatchesSegment(
|
| 92 |
}
|
| 93 |
|
| 94 |
public Task<List<MenuDto>> GetUserMenusAsync(ClaimsPrincipal user)
|
|
@@ -100,13 +101,41 @@ public class MenuAuthorizationService
|
|
| 100 |
|
| 101 |
var roleKey = GetRoleCacheKey(user);
|
| 102 |
|
| 103 |
-
if (_sessionState.
|
| 104 |
{
|
| 105 |
-
// Cache hit
|
| 106 |
-
return
|
| 107 |
}
|
| 108 |
|
| 109 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
public void PreloadMenus(ClaimsPrincipal user)
|
|
@@ -117,20 +146,20 @@ public class MenuAuthorizationService
|
|
| 117 |
}
|
| 118 |
|
| 119 |
var roleKey = GetRoleCacheKey(user);
|
| 120 |
-
if (_sessionState.
|
| 121 |
{
|
| 122 |
return;
|
| 123 |
}
|
| 124 |
|
| 125 |
-
_ =
|
| 126 |
}
|
| 127 |
|
| 128 |
/// <summary>
|
| 129 |
-
/// When
|
| 130 |
/// If the version changed, busts the cache and re-fetches the full menu list.
|
| 131 |
-
/// Falls back to the cached
|
| 132 |
/// </summary>
|
| 133 |
-
private async Task<List<MenuDto>>
|
| 134 |
{
|
| 135 |
try
|
| 136 |
{
|
|
@@ -141,95 +170,173 @@ public class MenuAuthorizationService
|
|
| 141 |
if (response.IsSuccessStatusCode)
|
| 142 |
{
|
| 143 |
var serverVersion = await response.Content.ReadAsStringAsync(cts.Token);
|
| 144 |
-
// Strip surrounding quotes that JSON serialisation may add
|
| 145 |
serverVersion = serverVersion.Trim('"');
|
| 146 |
|
| 147 |
-
if (serverVersion != _sessionState.
|
| 148 |
{
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
return await LoadMenusAsync(user, roleKey);
|
| 153 |
}
|
| 154 |
}
|
| 155 |
}
|
| 156 |
catch (Exception ex)
|
| 157 |
{
|
| 158 |
-
// If the version check fails, just use the cached menus to avoid breaking the UI.
|
| 159 |
Console.WriteLine($"[MenuAuth] Access version check failed (using cache): {ex.Message}");
|
| 160 |
}
|
| 161 |
|
| 162 |
-
return _sessionState.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
-
private Task<
|
| 166 |
{
|
| 167 |
-
if (
|
| 168 |
{
|
| 169 |
-
return
|
| 170 |
}
|
| 171 |
|
| 172 |
-
|
| 173 |
-
return
|
| 174 |
}
|
| 175 |
|
| 176 |
-
private async Task<
|
| 177 |
{
|
| 178 |
try
|
| 179 |
{
|
| 180 |
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
| 181 |
var client = _apiClient.CreateClient(user);
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
var versionTask = client.GetAsync("Menu/version", cts.Token);
|
| 186 |
|
| 187 |
-
await Task.WhenAll(
|
| 188 |
|
| 189 |
-
var menuResponse = await
|
| 190 |
if (!menuResponse.IsSuccessStatusCode)
|
| 191 |
{
|
| 192 |
Console.WriteLine($"Menu API failed: {(int)menuResponse.StatusCode} {menuResponse.ReasonPhrase}");
|
| 193 |
return new List<MenuDto>();
|
| 194 |
}
|
| 195 |
|
| 196 |
-
var
|
| 197 |
?? new List<MenuDto>();
|
| 198 |
|
| 199 |
-
if (
|
| 200 |
{
|
| 201 |
-
|
| 202 |
}
|
| 203 |
|
| 204 |
-
// Store the version so we can detect future access changes.
|
| 205 |
var versionResponse = await versionTask;
|
| 206 |
if (versionResponse.IsSuccessStatusCode)
|
| 207 |
{
|
| 208 |
var version = await versionResponse.Content.ReadAsStringAsync(cts.Token);
|
| 209 |
-
_sessionState.
|
| 210 |
}
|
| 211 |
|
| 212 |
-
_sessionState.
|
| 213 |
-
_sessionState.
|
| 214 |
-
return
|
| 215 |
}
|
| 216 |
catch (Exception ex)
|
| 217 |
{
|
| 218 |
Console.WriteLine($"Menu API error: {ex.Message}");
|
| 219 |
-
var
|
| 220 |
-
_sessionState.
|
| 221 |
-
_sessionState.
|
| 222 |
-
return
|
| 223 |
}
|
| 224 |
finally
|
| 225 |
{
|
| 226 |
-
|
| 227 |
}
|
| 228 |
}
|
| 229 |
|
| 230 |
-
private static bool MenuCollectionMatchesSegment(IReadOnlyList<MenuDto>
|
| 231 |
{
|
| 232 |
-
foreach (var menu in
|
| 233 |
{
|
| 234 |
if (MenuMatchesSegment(menu, segment))
|
| 235 |
{
|
|
@@ -259,6 +366,11 @@ public class MenuAuthorizationService
|
|
| 259 |
return menuSegment.Equals(segment, StringComparison.OrdinalIgnoreCase);
|
| 260 |
}
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
private static string GetRoleCacheKey(ClaimsPrincipal user)
|
| 263 |
{
|
| 264 |
var roleId = user.FindFirst("role_id")?.Value;
|
|
@@ -273,50 +385,6 @@ public class MenuAuthorizationService
|
|
| 273 |
return $"name:{roleName}";
|
| 274 |
}
|
| 275 |
|
| 276 |
-
return
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
private static List<MenuDto> BuildFallbackMenus(ClaimsPrincipal user)
|
| 280 |
-
{
|
| 281 |
-
var roleName = user.FindFirst(ClaimTypes.Role)?.Value ?? string.Empty;
|
| 282 |
-
|
| 283 |
-
var menus = new List<MenuDto>
|
| 284 |
-
{
|
| 285 |
-
CreateMenu("dashboard", "Dashboard", "/dashboard", 1, "layout-dashboard")
|
| 286 |
-
};
|
| 287 |
-
|
| 288 |
-
if (roleName.Equals("Employee", StringComparison.OrdinalIgnoreCase))
|
| 289 |
-
{
|
| 290 |
-
menus.Add(CreateMenu("tasks", "Tasks", "/tasks", 2, "list-checks"));
|
| 291 |
-
return menus;
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
menus.Add(CreateMenu("tasks", "Tasks", "/tasks", 2, "list-checks"));
|
| 295 |
-
menus.Add(CreateMenu("projects", "Projects", "/projects", 3, "folder-kanban"));
|
| 296 |
-
menus.Add(CreateMenu("reports", "Reports", "/reports/tasks", 4, "bar-chart-3"));
|
| 297 |
-
|
| 298 |
-
if (roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase) ||
|
| 299 |
-
roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 300 |
-
{
|
| 301 |
-
if (roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 302 |
-
{
|
| 303 |
-
menus.Add(CreateMenu("users", "Users", "/users", 5, "users"));
|
| 304 |
-
menus.Add(CreateMenu("roles", "Roles", "/roles", 6, "shield"));
|
| 305 |
-
}
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
return menus;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
private static MenuDto CreateMenu(string code, string name, string url, int orderNo, string icon)
|
| 312 |
-
{
|
| 313 |
-
return new MenuDto
|
| 314 |
-
{
|
| 315 |
-
MenuCode = code,
|
| 316 |
-
MenuName = name,
|
| 317 |
-
MenuUrl = url,
|
| 318 |
-
OrderNo = orderNo,
|
| 319 |
-
Icon = icon
|
| 320 |
-
};
|
| 321 |
}
|
| 322 |
}
|
|
|
|
| 17 |
|
| 18 |
private readonly ApiClientService _apiClient;
|
| 19 |
private readonly UserSessionState _sessionState;
|
| 20 |
+
private Task<List<MenuDto>>? _loadingAccessItems;
|
| 21 |
+
private Task<HashSet<string>>? _loadingAccessCodes;
|
| 22 |
|
| 23 |
public MenuAuthorizationService(ApiClientService apiClient, UserSessionState sessionState)
|
| 24 |
{
|
|
|
|
| 66 |
|
| 67 |
var roleKey = GetRoleCacheKey(user);
|
| 68 |
|
| 69 |
+
if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessItems != null)
|
| 70 |
{
|
| 71 |
+
return IsRouteAllowed(_sessionState.CachedAccessItems, relativePath);
|
| 72 |
}
|
| 73 |
|
| 74 |
return false;
|
| 75 |
}
|
| 76 |
|
| 77 |
+
public bool IsRouteAllowed(IReadOnlyList<MenuDto> accessItems, string relativePath)
|
| 78 |
{
|
| 79 |
var firstSegment = GetFirstSegment(relativePath);
|
| 80 |
|
|
|
|
| 86 |
if (firstSegment.Equals("dashboard", StringComparison.OrdinalIgnoreCase) ||
|
| 87 |
firstSegment.Equals("home", StringComparison.OrdinalIgnoreCase))
|
| 88 |
{
|
| 89 |
+
return MenuCollectionMatchesSegment(accessItems, "dashboard");
|
| 90 |
}
|
| 91 |
|
| 92 |
+
return MenuCollectionMatchesSegment(accessItems, firstSegment);
|
| 93 |
}
|
| 94 |
|
| 95 |
public Task<List<MenuDto>> GetUserMenusAsync(ClaimsPrincipal user)
|
|
|
|
| 101 |
|
| 102 |
var roleKey = GetRoleCacheKey(user);
|
| 103 |
|
| 104 |
+
if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessItems != null)
|
| 105 |
{
|
| 106 |
+
// Cache hit, but verify the access has not been changed by an admin.
|
| 107 |
+
return LoadAccessItemsWithVersionCheckAsync(user, roleKey);
|
| 108 |
}
|
| 109 |
|
| 110 |
+
return LoadAccessItemsAsync(user, roleKey);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
public async Task<bool> HasAccessCodeAsync(ClaimsPrincipal user, string accessCode)
|
| 114 |
+
{
|
| 115 |
+
if (string.IsNullOrWhiteSpace(accessCode))
|
| 116 |
+
{
|
| 117 |
+
return false;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
var codes = await GetCurrentAccessCodesAsync(user);
|
| 121 |
+
return codes.Contains(accessCode.Trim());
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
public Task<HashSet<string>> GetCurrentAccessCodesAsync(ClaimsPrincipal user)
|
| 125 |
+
{
|
| 126 |
+
if (user.Identity?.IsAuthenticated != true)
|
| 127 |
+
{
|
| 128 |
+
return Task.FromResult(new HashSet<string>(StringComparer.OrdinalIgnoreCase));
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
var roleKey = GetRoleCacheKey(user);
|
| 132 |
+
|
| 133 |
+
if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessCodes != null)
|
| 134 |
+
{
|
| 135 |
+
return LoadAccessCodesWithVersionCheckAsync(user, roleKey);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
return LoadAccessCodesAsync(user, roleKey);
|
| 139 |
}
|
| 140 |
|
| 141 |
public void PreloadMenus(ClaimsPrincipal user)
|
|
|
|
| 146 |
}
|
| 147 |
|
| 148 |
var roleKey = GetRoleCacheKey(user);
|
| 149 |
+
if (_sessionState.CachedAccessRoleId == roleKey && _sessionState.CachedAccessItems != null)
|
| 150 |
{
|
| 151 |
return;
|
| 152 |
}
|
| 153 |
|
| 154 |
+
_ = LoadAccessItemsAsync(user, roleKey);
|
| 155 |
}
|
| 156 |
|
| 157 |
/// <summary>
|
| 158 |
+
/// When access items are cached, quickly checks the server-side version to see if an admin changed permissions.
|
| 159 |
/// If the version changed, busts the cache and re-fetches the full menu list.
|
| 160 |
+
/// Falls back to the cached access items if the version check itself fails (e.g. network error).
|
| 161 |
/// </summary>
|
| 162 |
+
private async Task<List<MenuDto>> LoadAccessItemsWithVersionCheckAsync(ClaimsPrincipal user, string roleKey)
|
| 163 |
{
|
| 164 |
try
|
| 165 |
{
|
|
|
|
| 170 |
if (response.IsSuccessStatusCode)
|
| 171 |
{
|
| 172 |
var serverVersion = await response.Content.ReadAsStringAsync(cts.Token);
|
|
|
|
| 173 |
serverVersion = serverVersion.Trim('"');
|
| 174 |
|
| 175 |
+
if (serverVersion != _sessionState.CachedAccessVersion)
|
| 176 |
{
|
| 177 |
+
Console.WriteLine($"[MenuAuth] Access version changed ({_sessionState.CachedAccessVersion} -> {serverVersion}). Reloading access items.");
|
| 178 |
+
_sessionState.ClearAccessCache();
|
| 179 |
+
return await LoadAccessItemsAsync(user, roleKey);
|
|
|
|
| 180 |
}
|
| 181 |
}
|
| 182 |
}
|
| 183 |
catch (Exception ex)
|
| 184 |
{
|
|
|
|
| 185 |
Console.WriteLine($"[MenuAuth] Access version check failed (using cache): {ex.Message}");
|
| 186 |
}
|
| 187 |
|
| 188 |
+
return _sessionState.CachedAccessItems!;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
private Task<List<MenuDto>> LoadAccessItemsAsync(ClaimsPrincipal user, string roleKey)
|
| 192 |
+
{
|
| 193 |
+
if (_loadingAccessItems is { IsCompleted: false })
|
| 194 |
+
{
|
| 195 |
+
return _loadingAccessItems;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
_loadingAccessItems = FetchAccessItemsFromApiAsync(user, roleKey);
|
| 199 |
+
return _loadingAccessItems;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
private async Task<HashSet<string>> LoadAccessCodesWithVersionCheckAsync(ClaimsPrincipal user, string roleKey)
|
| 203 |
+
{
|
| 204 |
+
try
|
| 205 |
+
{
|
| 206 |
+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
| 207 |
+
var client = _apiClient.CreateClient(user);
|
| 208 |
+
var response = await client.GetAsync("Menu/version", cts.Token);
|
| 209 |
+
|
| 210 |
+
if (response.IsSuccessStatusCode)
|
| 211 |
+
{
|
| 212 |
+
var serverVersion = await response.Content.ReadAsStringAsync(cts.Token);
|
| 213 |
+
serverVersion = serverVersion.Trim('"');
|
| 214 |
+
|
| 215 |
+
if (serverVersion != _sessionState.CachedAccessVersion)
|
| 216 |
+
{
|
| 217 |
+
_sessionState.ClearAccessCache();
|
| 218 |
+
return await LoadAccessCodesAsync(user, roleKey);
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
catch (Exception ex)
|
| 223 |
+
{
|
| 224 |
+
Console.WriteLine($"[MenuAuth] Access code version check failed (using cache): {ex.Message}");
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
return _sessionState.CachedAccessCodes ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
| 228 |
}
|
| 229 |
|
| 230 |
+
private Task<HashSet<string>> LoadAccessCodesAsync(ClaimsPrincipal user, string roleKey)
|
| 231 |
{
|
| 232 |
+
if (_loadingAccessCodes is { IsCompleted: false })
|
| 233 |
{
|
| 234 |
+
return _loadingAccessCodes;
|
| 235 |
}
|
| 236 |
|
| 237 |
+
_loadingAccessCodes = FetchAccessCodesFromApiAsync(user, roleKey);
|
| 238 |
+
return _loadingAccessCodes;
|
| 239 |
}
|
| 240 |
|
| 241 |
+
private async Task<HashSet<string>> FetchAccessCodesFromApiAsync(ClaimsPrincipal user, string roleKey)
|
| 242 |
{
|
| 243 |
try
|
| 244 |
{
|
| 245 |
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
| 246 |
var client = _apiClient.CreateClient(user);
|
| 247 |
|
| 248 |
+
var codesResponse = await client.GetAsync("Menu/access-codes", cts.Token);
|
| 249 |
+
if (!codesResponse.IsSuccessStatusCode)
|
| 250 |
+
{
|
| 251 |
+
Console.WriteLine($"Menu access-codes API failed: {(int)codesResponse.StatusCode} {codesResponse.ReasonPhrase}");
|
| 252 |
+
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
var codes = await codesResponse.Content.ReadFromJsonAsync<List<string>>(cancellationToken: cts.Token)
|
| 256 |
+
?? new List<string>();
|
| 257 |
+
|
| 258 |
+
var versionResponse = await client.GetAsync("Menu/version", cts.Token);
|
| 259 |
+
if (versionResponse.IsSuccessStatusCode)
|
| 260 |
+
{
|
| 261 |
+
var version = await versionResponse.Content.ReadAsStringAsync(cts.Token);
|
| 262 |
+
_sessionState.CachedAccessVersion = version.Trim('"');
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
var codeSet = new HashSet<string>(codes.Where(code => !string.IsNullOrWhiteSpace(code)).Select(code => code.Trim()), StringComparer.OrdinalIgnoreCase);
|
| 266 |
+
_sessionState.CachedAccessCodes = codeSet;
|
| 267 |
+
_sessionState.CachedAccessRoleId = roleKey;
|
| 268 |
+
return codeSet;
|
| 269 |
+
}
|
| 270 |
+
catch (Exception ex)
|
| 271 |
+
{
|
| 272 |
+
Console.WriteLine($"Menu access-codes API error: {ex.Message}");
|
| 273 |
+
var fallback = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
| 274 |
+
_sessionState.CachedAccessCodes = fallback;
|
| 275 |
+
_sessionState.CachedAccessRoleId = roleKey;
|
| 276 |
+
return fallback;
|
| 277 |
+
}
|
| 278 |
+
finally
|
| 279 |
+
{
|
| 280 |
+
_loadingAccessCodes = null;
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
private async Task<List<MenuDto>> FetchAccessItemsFromApiAsync(ClaimsPrincipal user, string roleKey)
|
| 285 |
+
{
|
| 286 |
+
try
|
| 287 |
+
{
|
| 288 |
+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
| 289 |
+
var client = _apiClient.CreateClient(user);
|
| 290 |
+
|
| 291 |
+
// Fetch access items and version in parallel for efficiency.
|
| 292 |
+
var accessItemsTask = client.GetAsync("Menu", cts.Token);
|
| 293 |
var versionTask = client.GetAsync("Menu/version", cts.Token);
|
| 294 |
|
| 295 |
+
await Task.WhenAll(accessItemsTask, versionTask);
|
| 296 |
|
| 297 |
+
var menuResponse = await accessItemsTask;
|
| 298 |
if (!menuResponse.IsSuccessStatusCode)
|
| 299 |
{
|
| 300 |
Console.WriteLine($"Menu API failed: {(int)menuResponse.StatusCode} {menuResponse.ReasonPhrase}");
|
| 301 |
return new List<MenuDto>();
|
| 302 |
}
|
| 303 |
|
| 304 |
+
var accessItems = await menuResponse.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
|
| 305 |
?? new List<MenuDto>();
|
| 306 |
|
| 307 |
+
if (accessItems.Count == 0)
|
| 308 |
{
|
| 309 |
+
accessItems = BuildFallbackMenus(user);
|
| 310 |
}
|
| 311 |
|
|
|
|
| 312 |
var versionResponse = await versionTask;
|
| 313 |
if (versionResponse.IsSuccessStatusCode)
|
| 314 |
{
|
| 315 |
var version = await versionResponse.Content.ReadAsStringAsync(cts.Token);
|
| 316 |
+
_sessionState.CachedAccessVersion = version.Trim('"');
|
| 317 |
}
|
| 318 |
|
| 319 |
+
_sessionState.CachedAccessItems = accessItems;
|
| 320 |
+
_sessionState.CachedAccessRoleId = roleKey;
|
| 321 |
+
return accessItems;
|
| 322 |
}
|
| 323 |
catch (Exception ex)
|
| 324 |
{
|
| 325 |
Console.WriteLine($"Menu API error: {ex.Message}");
|
| 326 |
+
var fallbackAccessItems = BuildFallbackMenus(user);
|
| 327 |
+
_sessionState.CachedAccessItems = fallbackAccessItems;
|
| 328 |
+
_sessionState.CachedAccessRoleId = roleKey;
|
| 329 |
+
return fallbackAccessItems;
|
| 330 |
}
|
| 331 |
finally
|
| 332 |
{
|
| 333 |
+
_loadingAccessItems = null;
|
| 334 |
}
|
| 335 |
}
|
| 336 |
|
| 337 |
+
private static bool MenuCollectionMatchesSegment(IReadOnlyList<MenuDto> accessItems, string segment)
|
| 338 |
{
|
| 339 |
+
foreach (var menu in accessItems)
|
| 340 |
{
|
| 341 |
if (MenuMatchesSegment(menu, segment))
|
| 342 |
{
|
|
|
|
| 366 |
return menuSegment.Equals(segment, StringComparison.OrdinalIgnoreCase);
|
| 367 |
}
|
| 368 |
|
| 369 |
+
private static List<MenuDto> BuildFallbackMenus(ClaimsPrincipal user)
|
| 370 |
+
{
|
| 371 |
+
return new List<MenuDto>();
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
private static string GetRoleCacheKey(ClaimsPrincipal user)
|
| 375 |
{
|
| 376 |
var roleId = user.FindFirst("role_id")?.Value;
|
|
|
|
| 385 |
return $"name:{roleName}";
|
| 386 |
}
|
| 387 |
|
| 388 |
+
return "unknown";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
}
|
| 390 |
}
|
TaskTrackingSystem.WebApp/UserSessionState.cs
CHANGED
|
@@ -8,26 +8,29 @@ public class UserSessionState
|
|
| 8 |
public string? Token { get; set; }
|
| 9 |
public ClaimsPrincipal? CachedUser { get; set; }
|
| 10 |
|
| 11 |
-
public string?
|
| 12 |
-
public List<MenuDto>?
|
|
|
|
| 13 |
|
| 14 |
/// <summary>
|
| 15 |
-
/// The server-side permissions version (latest role access timestamp) when the
|
| 16 |
/// Used to detect when an admin has changed a role's permissions so the cache can be busted.
|
| 17 |
/// </summary>
|
| 18 |
-
public string?
|
| 19 |
|
| 20 |
-
public void
|
| 21 |
{
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
public void ClearSession()
|
| 28 |
{
|
| 29 |
Token = null;
|
| 30 |
CachedUser = null;
|
| 31 |
-
|
| 32 |
}
|
| 33 |
}
|
|
|
|
|
|
| 8 |
public string? Token { get; set; }
|
| 9 |
public ClaimsPrincipal? CachedUser { get; set; }
|
| 10 |
|
| 11 |
+
public string? CachedAccessRoleId { get; set; }
|
| 12 |
+
public List<MenuDto>? CachedAccessItems { get; set; }
|
| 13 |
+
public HashSet<string>? CachedAccessCodes { get; set; }
|
| 14 |
|
| 15 |
/// <summary>
|
| 16 |
+
/// The server-side permissions version (latest role access timestamp) when the access items were last fetched.
|
| 17 |
/// Used to detect when an admin has changed a role's permissions so the cache can be busted.
|
| 18 |
/// </summary>
|
| 19 |
+
public string? CachedAccessVersion { get; set; }
|
| 20 |
|
| 21 |
+
public void ClearAccessCache()
|
| 22 |
{
|
| 23 |
+
CachedAccessRoleId = null;
|
| 24 |
+
CachedAccessItems = null;
|
| 25 |
+
CachedAccessCodes = null;
|
| 26 |
+
CachedAccessVersion = null;
|
| 27 |
}
|
| 28 |
|
| 29 |
public void ClearSession()
|
| 30 |
{
|
| 31 |
Token = null;
|
| 32 |
CachedUser = null;
|
| 33 |
+
ClearAccessCache();
|
| 34 |
}
|
| 35 |
}
|
| 36 |
+
|
cleanup-legacy-access-schema.sql
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
Legacy access-schema cleanup script for SQL Server Management Studio.
|
| 3 |
+
|
| 4 |
+
Run this only after:
|
| 5 |
+
1. You have a backup of the database.
|
| 6 |
+
2. The new schema is already in place.
|
| 7 |
+
3. You have confirmed that dbo.Menus, dbo.Permissions, dbo.RoleMenus, and dbo.RolePermissions are working.
|
| 8 |
+
|
| 9 |
+
This script removes the old scaffolded tables:
|
| 10 |
+
- dbo.MenuAdmins
|
| 11 |
+
- dbo.MenuAdminDetails
|
| 12 |
+
- dbo.RoleMenus_Legacy
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
SET XACT_ABORT ON;
|
| 16 |
+
BEGIN TRY
|
| 17 |
+
BEGIN TRANSACTION;
|
| 18 |
+
|
| 19 |
+
IF OBJECT_ID('dbo.RoleMenus_Legacy', 'U') IS NOT NULL
|
| 20 |
+
BEGIN
|
| 21 |
+
DROP TABLE dbo.RoleMenus_Legacy;
|
| 22 |
+
END
|
| 23 |
+
|
| 24 |
+
IF OBJECT_ID('dbo.MenuAdminDetails', 'U') IS NOT NULL
|
| 25 |
+
BEGIN
|
| 26 |
+
DROP TABLE dbo.MenuAdminDetails;
|
| 27 |
+
END
|
| 28 |
+
|
| 29 |
+
IF OBJECT_ID('dbo.MenuAdmins', 'U') IS NOT NULL
|
| 30 |
+
BEGIN
|
| 31 |
+
DROP TABLE dbo.MenuAdmins;
|
| 32 |
+
END
|
| 33 |
+
|
| 34 |
+
COMMIT TRANSACTION;
|
| 35 |
+
END TRY
|
| 36 |
+
BEGIN CATCH
|
| 37 |
+
IF @@TRANCOUNT > 0
|
| 38 |
+
ROLLBACK TRANSACTION;
|
| 39 |
+
|
| 40 |
+
THROW;
|
| 41 |
+
END CATCH;
|