User commited on
Commit
72f2be7
·
1 Parent(s): 631e3a2

fix: match the menu access and permissions

Browse files
Files changed (26) hide show
  1. TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -46
  2. TaskTrackingSystem.Database/AppDbContextModels/MenuAdmin.cs +0 -33
  3. TaskTrackingSystem.Database/AppDbContextModels/MenuAdminDetail.cs +0 -31
  4. TaskTrackingSystem.Database/AppDbContextModels/RoleMenusLegacy.cs +0 -25
  5. TaskTrackingSystem.Shared/Models/Menu/AccessPermissionDto.cs +2 -2
  6. TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +9 -0
  7. TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs +34 -2
  8. TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs +28 -6
  9. TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +40 -2
  10. TaskTrackingSystem.WebApi/Features/Task/TaskController.cs +23 -1
  11. TaskTrackingSystem.WebApi/Features/User/UserController.cs +19 -4
  12. TaskTrackingSystem.WebApi/Infrastructure/PermissionAuthorizationService.cs +62 -0
  13. TaskTrackingSystem.WebApi/Program.cs +1 -0
  14. TaskTrackingSystem.WebApp/Components/CustomAuthenticationStateProvider.cs +3 -2
  15. TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +2 -2
  16. TaskTrackingSystem.WebApp/Components/Pages/Home.razor +10 -3
  17. TaskTrackingSystem.WebApp/Components/Pages/KanbanBoard.razor +13 -4
  18. TaskTrackingSystem.WebApp/Components/Pages/ProjectAssign.razor +13 -5
  19. TaskTrackingSystem.WebApp/Components/Pages/Projects.razor +34 -12
  20. TaskTrackingSystem.WebApp/Components/Pages/Roles.razor +55 -30
  21. TaskTrackingSystem.WebApp/Components/Pages/TaskAssign.razor +13 -5
  22. TaskTrackingSystem.WebApp/Components/Pages/Tasks.razor +52 -20
  23. TaskTrackingSystem.WebApp/Components/Pages/Users.razor +29 -10
  24. TaskTrackingSystem.WebApp/MenuAuthorizationService.cs +161 -93
  25. TaskTrackingSystem.WebApp/UserSessionState.cs +12 -9
  26. 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 MenuAdminDetailId { get; set; } = string.Empty;
8
- public string MenuDetailCode { 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;
 
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
- MenuAdminDetailId = permission.PermissionId.ToString(),
265
- MenuDetailCode = permission.PermissionCode,
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(Roles = "Admin")]
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.CachedMenuRoleId = $"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.CachedMenuRoleId = $"name:{roleName}";
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 menu items are available for this role.
55
  </div>
56
  }
57
  </nav>
@@ -126,7 +126,7 @@
126
  }
127
  else
128
  {
129
- // Menus 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))
 
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
- <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">
43
- <i data-lucide="plus" class="h-4 w-4 mr-2"></i> New Task
44
- </a>
 
 
 
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
- <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">
32
- <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
33
- New Task
34
- </button>
 
 
 
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
- <button @onclick="RequestSaveConfirmation" disabled="@(isSaving || isLoadingMembers)"
190
- 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">
191
- <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
192
- @(isSaving ? "Saving changes..." : "Save Assignment Changes")
193
- </button>
 
 
 
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 (isAdmin)
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 (isAdmin)
94
  {
95
- <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">
96
- <i data-lucide="user-plus" class="h-4 w-4"></i>
97
- </button>
98
- <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">
99
- <i data-lucide="pencil" class="h-4 w-4"></i>
100
- </button>
101
- <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">
102
- <i data-lucide="trash-2" class="h-4 w-4"></i>
103
- </button>
 
 
 
 
 
 
 
 
 
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="@(isAdmin ? "Create your first project to get started." : "You have not been assigned to any projects.")" />
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
- <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">
23
- <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
24
- New Role
25
- </button>
 
 
 
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
- <button @onclick="() => OpenAccessModal(role)"
81
- 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"
82
- title="Manage Access">
83
- <i data-lucide="shield-check" class="h-3.5 w-3.5"></i>
84
- Access
85
- </button>
86
- <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">
87
- <i data-lucide="pencil" class="h-4 w-4"></i>
88
- </button>
89
- <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">
90
- <i data-lucide="trash-2" class="h-4 w-4"></i>
91
- </button>
 
 
 
 
 
 
 
 
 
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.MenuDetailCode);
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.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
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.MenuDetailCode);
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.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
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.MenuDetailCode);
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.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
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.MenuDetailCode);
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.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
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.MenuDetailCode);
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.MenuDetailCode);
713
  }
714
  }
715
  }
@@ -719,7 +744,7 @@
719
  // Unselect direct actions
720
  foreach (var action in menu.Permissions)
721
  {
722
- targetSet.Remove(action.MenuDetailCode);
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.MenuDetailCode);
732
  }
733
  }
734
  }
@@ -760,7 +785,7 @@
760
  formSelectedAccessCodes.Add(menu.MenuCode);
761
  foreach (var action in menu.Permissions)
762
  {
763
- formSelectedAccessCodes.Add(action.MenuDetailCode);
764
  }
765
  }
766
  }
@@ -951,7 +976,7 @@
951
  selectedAccessCodes.Add(menu.MenuCode);
952
  foreach (var action in menu.Permissions)
953
  {
954
- selectedAccessCodes.Add(action.MenuDetailCode);
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
- <button @onclick="RequestSaveConfirmation" disabled="@isSaving"
225
- 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">
226
- <i data-lucide="user-check" class="h-4 w-4 mr-2"></i>
227
- @(isSaving ? "Saving..." : "Save Assignment")
228
- </button>
 
 
 
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
- <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">
41
- <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
42
- New Task
43
- </button>
 
 
 
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
- <button @onclick="() => OpenCreateModalForProject(proj.Id)" class="inline-flex items-center text-xs font-semibold text-emerald-600 hover:text-emerald-800 transition-colors">
112
- <i data-lucide="plus" class="h-3 w-3 mr-1"></i> Add Task
113
- </button>
 
 
 
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
- <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">
150
- <i data-lucide="pencil" class="h-3.5 w-3.5"></i>
151
- </button>
152
- <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">
153
- <i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
154
- </button>
 
 
 
 
 
 
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
- <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">
297
- <i data-lucide="pencil" class="h-4 w-4"></i>
298
- </button>
299
- <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">
300
- <i data-lucide="trash-2" class="h-4 w-4"></i>
301
- </button>
 
 
 
 
 
 
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
- <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">
22
- <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
23
- New User
24
- </button>
 
 
 
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
- <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">
115
- <i data-lucide="pencil" class="h-4 w-4"></i>
116
- </button>
117
- <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">
118
- <i data-lucide="trash-2" class="h-4 w-4"></i>
119
- </button>
 
 
 
 
 
 
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>>? _loadingMenus;
 
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.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
69
  {
70
- return IsRouteAllowed(_sessionState.CachedMenus, relativePath);
71
  }
72
 
73
  return false;
74
  }
75
 
76
- public bool IsRouteAllowed(IReadOnlyList<MenuDto> menus, string relativePath)
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(menus, "dashboard");
89
  }
90
 
91
- return MenuCollectionMatchesSegment(menus, firstSegment);
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.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
104
  {
105
- // Cache hit but verify the permissions haven't been changed by an admin.
106
- return LoadMenusWithVersionCheckAsync(user, roleKey);
107
  }
108
 
109
- return LoadMenusAsync(user, roleKey);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
121
  {
122
  return;
123
  }
124
 
125
- _ = LoadMenusAsync(user, roleKey);
126
  }
127
 
128
  /// <summary>
129
- /// When menus are cached, quickly checks the server-side version to see if an admin changed permissions.
130
  /// If the version changed, busts the cache and re-fetches the full menu list.
131
- /// Falls back to the cached menus if the version check itself fails (e.g. network error).
132
  /// </summary>
133
- private async Task<List<MenuDto>> LoadMenusWithVersionCheckAsync(ClaimsPrincipal user, string roleKey)
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.CachedMenuVersion)
148
  {
149
- // Access items have changed bust cache and reload the menu tree.
150
- Console.WriteLine($"[MenuAuth] Access version changed ({_sessionState.CachedMenuVersion} -> {serverVersion}). Reloading menus.");
151
- _sessionState.ClearMenuCache();
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.CachedMenus!;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
164
 
165
- private Task<List<MenuDto>> LoadMenusAsync(ClaimsPrincipal user, string roleKey)
166
  {
167
- if (_loadingMenus is { IsCompleted: false })
168
  {
169
- return _loadingMenus;
170
  }
171
 
172
- _loadingMenus = FetchMenusFromApiAsync(user, roleKey);
173
- return _loadingMenus;
174
  }
175
 
176
- private async Task<List<MenuDto>> FetchMenusFromApiAsync(ClaimsPrincipal user, string roleKey)
177
  {
178
  try
179
  {
180
  using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
181
  var client = _apiClient.CreateClient(user);
182
 
183
- // Fetch menus and version in parallel for efficiency.
184
- var menuTask = client.GetAsync("Menu", cts.Token);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  var versionTask = client.GetAsync("Menu/version", cts.Token);
186
 
187
- await Task.WhenAll(menuTask, versionTask);
188
 
189
- var menuResponse = await menuTask;
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 menus = await menuResponse.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
197
  ?? new List<MenuDto>();
198
 
199
- if (menus.Count == 0)
200
  {
201
- menus = BuildFallbackMenus(user);
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.CachedMenuVersion = version.Trim('"');
210
  }
211
 
212
- _sessionState.CachedMenus = menus;
213
- _sessionState.CachedMenuRoleId = roleKey;
214
- return menus;
215
  }
216
  catch (Exception ex)
217
  {
218
  Console.WriteLine($"Menu API error: {ex.Message}");
219
- var fallbackMenus = BuildFallbackMenus(user);
220
- _sessionState.CachedMenus = fallbackMenus;
221
- _sessionState.CachedMenuRoleId = roleKey;
222
- return fallbackMenus;
223
  }
224
  finally
225
  {
226
- _loadingMenus = null;
227
  }
228
  }
229
 
230
- private static bool MenuCollectionMatchesSegment(IReadOnlyList<MenuDto> menus, string segment)
231
  {
232
- foreach (var menu in menus)
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 string.Empty;
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? CachedMenuRoleId { get; set; }
12
- public List<MenuDto>? CachedMenus { get; set; }
 
13
 
14
  /// <summary>
15
- /// The server-side permissions version (latest role access timestamp) when the menus were last fetched.
16
  /// Used to detect when an admin has changed a role's permissions so the cache can be busted.
17
  /// </summary>
18
- public string? CachedMenuVersion { get; set; }
19
 
20
- public void ClearMenuCache()
21
  {
22
- CachedMenuRoleId = null;
23
- CachedMenus = null;
24
- CachedMenuVersion = null;
 
25
  }
26
 
27
  public void ClearSession()
28
  {
29
  Token = null;
30
  CachedUser = null;
31
- ClearMenuCache();
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;