Spaces:
Running
Running
User commited on
Commit ·
01be06d
1
Parent(s): 33d40ba
feat: clean out authentication and authorization, rolemenu database fixed(use role id instead of rolecode now), adjust pages
Browse files- TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +4 -3
- TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs +3 -1
- TaskTrackingSystem.Shared/Models/Auth/AuthResponse.cs +2 -1
- TaskTrackingSystem.Shared/Models/Auth/ResetPasswordDto.cs +18 -0
- TaskTrackingSystem.WebApi/Features/Auth/AuthController.cs +13 -1
- TaskTrackingSystem.WebApi/Features/Auth/AuthService.cs +68 -13
- TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs +4 -3
- TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs +47 -15
- TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +14 -4
- TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs +156 -18
- TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs +14 -8
- TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs +46 -16
- TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +1 -5
- TaskTrackingSystem.WebApi/Features/Role/RoleService.cs +73 -21
- TaskTrackingSystem.WebApi/Features/Task/TaskController.cs +9 -8
- TaskTrackingSystem.WebApi/Features/Task/TaskService.cs +59 -10
- TaskTrackingSystem.WebApi/Features/User/UserService.cs +10 -7
- TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs +33 -0
- TaskTrackingSystem.WebApi/Program.cs +3 -1
- TaskTrackingSystem.WebApi/appsettings.json +4 -1
- TaskTrackingSystem.WebApp/AccountEndpoints.cs +56 -3
- TaskTrackingSystem.WebApp/Components/CustomAuthenticationStateProvider.cs +13 -0
- TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +2 -2
- TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor +36 -218
- TaskTrackingSystem.WebApp/Components/Pages/Login.razor +13 -0
- TaskTrackingSystem.WebApp/Components/Pages/Projects.razor +1 -17
- TaskTrackingSystem.WebApp/Components/Pages/Reports.razor +0 -292
- TaskTrackingSystem.WebApp/Components/Pages/ResetPassword.razor +65 -0
- TaskTrackingSystem.WebApp/MenuAuthorizationService.cs +85 -46
- TaskTrackingSystem.WebApp/UserSessionState.cs +2 -2
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs
CHANGED
|
@@ -42,8 +42,8 @@ public partial class AppDbContext : DbContext
|
|
| 42 |
public virtual DbSet<User> Users { get; set; }
|
| 43 |
|
| 44 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 49 |
{
|
|
@@ -184,8 +184,9 @@ public partial class AppDbContext : DbContext
|
|
| 184 |
entity.Property(e => e.RoleMenuId).HasMaxLength(50);
|
| 185 |
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 186 |
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
| 187 |
-
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 188 |
entity.Property(e => e.RoleCode).HasMaxLength(50);
|
|
|
|
|
|
|
| 189 |
});
|
| 190 |
|
| 191 |
modelBuilder.Entity<Task>(entity =>
|
|
|
|
| 42 |
public virtual DbSet<User> Users { get; set; }
|
| 43 |
|
| 44 |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
| 45 |
+
{
|
| 46 |
+
}
|
| 47 |
|
| 48 |
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
| 49 |
{
|
|
|
|
| 184 |
entity.Property(e => e.RoleMenuId).HasMaxLength(50);
|
| 185 |
entity.Property(e => e.CreatedUserId).HasMaxLength(50);
|
| 186 |
entity.Property(e => e.MenuCode).HasMaxLength(50);
|
|
|
|
| 187 |
entity.Property(e => e.RoleCode).HasMaxLength(50);
|
| 188 |
+
entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
|
| 189 |
+
entity.HasIndex(e => e.RoleId, "IX_RoleMenus_RoleId");
|
| 190 |
});
|
| 191 |
|
| 192 |
modelBuilder.Entity<Task>(entity =>
|
TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs
CHANGED
|
@@ -7,7 +7,9 @@ public partial class RoleMenu
|
|
| 7 |
{
|
| 8 |
public string RoleMenuId { get; set; } = null!;
|
| 9 |
|
| 10 |
-
public
|
|
|
|
|
|
|
| 11 |
|
| 12 |
public string MenuCode { get; set; } = null!;
|
| 13 |
|
|
|
|
| 7 |
{
|
| 8 |
public string RoleMenuId { get; set; } = null!;
|
| 9 |
|
| 10 |
+
public long RoleId { get; set; }
|
| 11 |
+
|
| 12 |
+
public string? RoleCode { get; set; }
|
| 13 |
|
| 14 |
public string MenuCode { get; set; } = null!;
|
| 15 |
|
TaskTrackingSystem.Shared/Models/Auth/AuthResponse.cs
CHANGED
|
@@ -11,6 +11,7 @@ namespace TaskTrackingSystem.Shared.Models.Auth
|
|
| 11 |
public string Token { get; set; } = string.Empty;
|
| 12 |
public string Username { get; set; } = string.Empty;
|
| 13 |
public string Email { get; set; } = string.Empty;
|
|
|
|
| 14 |
public string RoleName { get; set; } = string.Empty;
|
| 15 |
}
|
| 16 |
-
}
|
|
|
|
| 11 |
public string Token { get; set; } = string.Empty;
|
| 12 |
public string Username { get; set; } = string.Empty;
|
| 13 |
public string Email { get; set; } = string.Empty;
|
| 14 |
+
public long RoleId { get; set; }
|
| 15 |
public string RoleName { get; set; } = string.Empty;
|
| 16 |
}
|
| 17 |
+
}
|
TaskTrackingSystem.Shared/Models/Auth/ResetPasswordDto.cs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System.ComponentModel.DataAnnotations;
|
| 2 |
+
|
| 3 |
+
namespace TaskTrackingSystem.Shared.Models.Auth
|
| 4 |
+
{
|
| 5 |
+
public class ResetPasswordDto
|
| 6 |
+
{
|
| 7 |
+
[Required]
|
| 8 |
+
public string UsernameOrEmail { get; set; } = string.Empty;
|
| 9 |
+
|
| 10 |
+
[Required]
|
| 11 |
+
public string RecoveryCode { get; set; } = string.Empty;
|
| 12 |
+
|
| 13 |
+
[Required]
|
| 14 |
+
[MinLength(8, ErrorMessage = ResultMessages.PasswordMinLengthRule)]
|
| 15 |
+
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$", ErrorMessage = ResultMessages.PasswordComplexityRule)]
|
| 16 |
+
public string NewPassword { get; set; } = string.Empty;
|
| 17 |
+
}
|
| 18 |
+
}
|
TaskTrackingSystem.WebApi/Features/Auth/AuthController.cs
CHANGED
|
@@ -52,5 +52,17 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 52 |
return BadRequest(Result<AuthResponseDto>.Failure(ex.Message, 400));
|
| 53 |
}
|
| 54 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
-
}
|
|
|
|
| 52 |
return BadRequest(Result<AuthResponseDto>.Failure(ex.Message, 400));
|
| 53 |
}
|
| 54 |
}
|
| 55 |
+
|
| 56 |
+
[HttpPost("reset-password")]
|
| 57 |
+
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto resetPasswordDto)
|
| 58 |
+
{
|
| 59 |
+
var result = await _authService.ResetPasswordAsync(resetPasswordDto);
|
| 60 |
+
if (!result.IsSuccess)
|
| 61 |
+
{
|
| 62 |
+
return StatusCode(result.StatusCode, result);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return Ok(result);
|
| 66 |
+
}
|
| 67 |
}
|
| 68 |
+
}
|
TaskTrackingSystem.WebApi/Features/Auth/AuthService.cs
CHANGED
|
@@ -4,8 +4,10 @@ using System.Security.Claims;
|
|
| 4 |
using System.Text;
|
| 5 |
using System.Threading.Tasks;
|
| 6 |
using Microsoft.EntityFrameworkCore;
|
|
|
|
| 7 |
using Microsoft.Extensions.Configuration;
|
| 8 |
using Microsoft.IdentityModel.Tokens;
|
|
|
|
| 9 |
using TaskTrackingSystem.Shared.Models.Auth;
|
| 10 |
using TaskTrackingSystem.Database;
|
| 11 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
|
@@ -16,11 +18,13 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 16 |
{
|
| 17 |
private readonly AppDbContext _db;
|
| 18 |
private readonly IConfiguration _configuration;
|
|
|
|
| 19 |
|
| 20 |
-
public AuthService(AppDbContext db, IConfiguration configuration)
|
| 21 |
{
|
| 22 |
_db = db;
|
| 23 |
_configuration = configuration;
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
public async Task<AuthResponseDto?> LoginAsync(LoginDto loginDto)
|
|
@@ -40,21 +44,34 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 40 |
}
|
| 41 |
|
| 42 |
|
| 43 |
-
|
| 44 |
-
if (
|
| 45 |
{
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
return new AuthResponseDto
|
| 53 |
{
|
| 54 |
Token = token,
|
| 55 |
Username = user.Username,
|
| 56 |
Email = user.Email,
|
| 57 |
-
|
|
|
|
| 58 |
};
|
| 59 |
}
|
| 60 |
|
|
@@ -80,36 +97,68 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 80 |
throw new InvalidOperationException("No system roles configured. Registration failed.");
|
| 81 |
}
|
| 82 |
|
| 83 |
-
string fakePasswordHash = "HASHED_" + dto.Password;
|
| 84 |
-
|
| 85 |
var user = new TaskTrackingSystem.Database.AppDbContextModels.User
|
| 86 |
{
|
| 87 |
Username = dto.Username,
|
| 88 |
FirstName = dto.FirstName,
|
| 89 |
LastName = dto.LastName,
|
| 90 |
Email = dto.Email,
|
| 91 |
-
PasswordHash =
|
| 92 |
Phone = dto.Phone,
|
| 93 |
RoleId = defaultRole.Id,
|
| 94 |
IsActive = true,
|
| 95 |
CreatedAt = DateTime.UtcNow
|
| 96 |
};
|
| 97 |
|
|
|
|
|
|
|
| 98 |
_db.Users.Add(user);
|
| 99 |
await _db.SaveChangesAsync();
|
| 100 |
|
| 101 |
-
var token = GenerateJwtToken(user.Id.ToString(), user.Username, user.Email, defaultRole.Name);
|
| 102 |
|
| 103 |
return new AuthResponseDto
|
| 104 |
{
|
| 105 |
Token = token,
|
| 106 |
Username = user.Username,
|
| 107 |
Email = user.Email,
|
|
|
|
| 108 |
RoleName = defaultRole.Name
|
| 109 |
};
|
| 110 |
}
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
{
|
| 114 |
var jwtSettings = _configuration.GetSection("JwtSettings");
|
| 115 |
var keyStr = jwtSettings["Key"] ?? "SUPER_SECRET_KEY_THAT_IS_LONG_ENOUGH_12345";
|
|
@@ -121,6 +170,7 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 121 |
new Claim(ClaimTypes.NameIdentifier, userId),
|
| 122 |
new Claim(ClaimTypes.Name, username),
|
| 123 |
new Claim(ClaimTypes.Email, email),
|
|
|
|
| 124 |
new Claim(ClaimTypes.Role, roleName)
|
| 125 |
};
|
| 126 |
|
|
@@ -134,5 +184,10 @@ namespace TaskTrackingSystem.WebApi.Features.Auth
|
|
| 134 |
|
| 135 |
return new JwtSecurityTokenHandler().WriteToken(token);
|
| 136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
-
}
|
|
|
|
| 4 |
using System.Text;
|
| 5 |
using System.Threading.Tasks;
|
| 6 |
using Microsoft.EntityFrameworkCore;
|
| 7 |
+
using Microsoft.AspNetCore.Identity;
|
| 8 |
using Microsoft.Extensions.Configuration;
|
| 9 |
using Microsoft.IdentityModel.Tokens;
|
| 10 |
+
using TaskTrackingSystem.Shared;
|
| 11 |
using TaskTrackingSystem.Shared.Models.Auth;
|
| 12 |
using TaskTrackingSystem.Database;
|
| 13 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
|
|
|
| 18 |
{
|
| 19 |
private readonly AppDbContext _db;
|
| 20 |
private readonly IConfiguration _configuration;
|
| 21 |
+
private readonly IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User> _passwordHasher;
|
| 22 |
|
| 23 |
+
public AuthService(AppDbContext db, IConfiguration configuration, IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User> passwordHasher)
|
| 24 |
{
|
| 25 |
_db = db;
|
| 26 |
_configuration = configuration;
|
| 27 |
+
_passwordHasher = passwordHasher;
|
| 28 |
}
|
| 29 |
|
| 30 |
public async Task<AuthResponseDto?> LoginAsync(LoginDto loginDto)
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
|
| 47 |
+
var passwordCheck = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, loginDto.Password);
|
| 48 |
+
if (passwordCheck == PasswordVerificationResult.Failed)
|
| 49 |
{
|
| 50 |
+
if (!IsLegacyPasswordMatch(user.PasswordHash, loginDto.Password))
|
| 51 |
+
{
|
| 52 |
+
return null;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
user.PasswordHash = _passwordHasher.HashPassword(user, loginDto.Password);
|
| 56 |
+
await _db.SaveChangesAsync();
|
| 57 |
}
|
| 58 |
|
| 59 |
+
var roleId = user.RoleId;
|
| 60 |
+
if (roleId <= 0 && user.Role != null)
|
| 61 |
+
{
|
| 62 |
+
roleId = user.Role.Id;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
var roleName = user.Role?.Name ?? string.Empty;
|
| 66 |
+
var token = GenerateJwtToken(user.Id.ToString(), user.Username, user.Email, roleId, roleName);
|
| 67 |
|
| 68 |
return new AuthResponseDto
|
| 69 |
{
|
| 70 |
Token = token,
|
| 71 |
Username = user.Username,
|
| 72 |
Email = user.Email,
|
| 73 |
+
RoleId = roleId,
|
| 74 |
+
RoleName = roleName
|
| 75 |
};
|
| 76 |
}
|
| 77 |
|
|
|
|
| 97 |
throw new InvalidOperationException("No system roles configured. Registration failed.");
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
| 100 |
var user = new TaskTrackingSystem.Database.AppDbContextModels.User
|
| 101 |
{
|
| 102 |
Username = dto.Username,
|
| 103 |
FirstName = dto.FirstName,
|
| 104 |
LastName = dto.LastName,
|
| 105 |
Email = dto.Email,
|
| 106 |
+
PasswordHash = string.Empty,
|
| 107 |
Phone = dto.Phone,
|
| 108 |
RoleId = defaultRole.Id,
|
| 109 |
IsActive = true,
|
| 110 |
CreatedAt = DateTime.UtcNow
|
| 111 |
};
|
| 112 |
|
| 113 |
+
user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password);
|
| 114 |
+
|
| 115 |
_db.Users.Add(user);
|
| 116 |
await _db.SaveChangesAsync();
|
| 117 |
|
| 118 |
+
var token = GenerateJwtToken(user.Id.ToString(), user.Username, user.Email, defaultRole.Id, defaultRole.Name);
|
| 119 |
|
| 120 |
return new AuthResponseDto
|
| 121 |
{
|
| 122 |
Token = token,
|
| 123 |
Username = user.Username,
|
| 124 |
Email = user.Email,
|
| 125 |
+
RoleId = defaultRole.Id,
|
| 126 |
RoleName = defaultRole.Name
|
| 127 |
};
|
| 128 |
}
|
| 129 |
|
| 130 |
+
public async Task<Result> ResetPasswordAsync(ResetPasswordDto dto)
|
| 131 |
+
{
|
| 132 |
+
if (string.IsNullOrWhiteSpace(dto.UsernameOrEmail) ||
|
| 133 |
+
string.IsNullOrWhiteSpace(dto.RecoveryCode) ||
|
| 134 |
+
string.IsNullOrWhiteSpace(dto.NewPassword))
|
| 135 |
+
{
|
| 136 |
+
return Result.Failure(ResultMessages.FillAllFields, 400);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
var expectedCode = _configuration["PasswordReset:RecoveryCode"] ?? "TASKIFY-RESET-2026";
|
| 140 |
+
if (!string.Equals(dto.RecoveryCode.Trim(), expectedCode, StringComparison.Ordinal))
|
| 141 |
+
{
|
| 142 |
+
return Result.Failure("Invalid recovery code.", 400);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
var user = await _db.Users
|
| 146 |
+
.FirstOrDefaultAsync(u => (u.Username == dto.UsernameOrEmail || u.Email == dto.UsernameOrEmail) && !u.IsDeleted);
|
| 147 |
+
|
| 148 |
+
if (user == null)
|
| 149 |
+
{
|
| 150 |
+
return Result.Failure("Account not found.", 404);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
user.PasswordHash = _passwordHasher.HashPassword(user, dto.NewPassword);
|
| 154 |
+
user.UpdatedAt = DateTime.UtcNow;
|
| 155 |
+
_db.Users.Update(user);
|
| 156 |
+
await _db.SaveChangesAsync();
|
| 157 |
+
|
| 158 |
+
return Result.Success(200);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
private string GenerateJwtToken(string userId, string username, string email, long roleId, string roleName)
|
| 162 |
{
|
| 163 |
var jwtSettings = _configuration.GetSection("JwtSettings");
|
| 164 |
var keyStr = jwtSettings["Key"] ?? "SUPER_SECRET_KEY_THAT_IS_LONG_ENOUGH_12345";
|
|
|
|
| 170 |
new Claim(ClaimTypes.NameIdentifier, userId),
|
| 171 |
new Claim(ClaimTypes.Name, username),
|
| 172 |
new Claim(ClaimTypes.Email, email),
|
| 173 |
+
new Claim("role_id", roleId.ToString()),
|
| 174 |
new Claim(ClaimTypes.Role, roleName)
|
| 175 |
};
|
| 176 |
|
|
|
|
| 184 |
|
| 185 |
return new JwtSecurityTokenHandler().WriteToken(token);
|
| 186 |
}
|
| 187 |
+
|
| 188 |
+
private static bool IsLegacyPasswordMatch(string passwordHash, string password)
|
| 189 |
+
{
|
| 190 |
+
return string.Equals(passwordHash, "HASHED_" + password, StringComparison.Ordinal);
|
| 191 |
+
}
|
| 192 |
}
|
| 193 |
+
}
|
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardController.cs
CHANGED
|
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|
| 4 |
using System.Threading.Tasks;
|
| 5 |
using TaskTrackingSystem.Shared;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Dashboard;
|
|
|
|
| 7 |
|
| 8 |
namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
| 9 |
{
|
|
@@ -22,21 +23,21 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
|
| 22 |
[HttpGet("summary")]
|
| 23 |
public async Task<ActionResult<Result<DashboardSummaryDto>>> GetSummary()
|
| 24 |
{
|
| 25 |
-
var result = await _dashboardService.GetSummaryAsync(
|
| 26 |
return StatusCode(result.StatusCode, result);
|
| 27 |
}
|
| 28 |
|
| 29 |
[HttpGet("tasks-overview")]
|
| 30 |
public async Task<ActionResult<Result<IEnumerable<TaskStatusOverviewDto>>>> GetTasksOverview()
|
| 31 |
{
|
| 32 |
-
var result = await _dashboardService.GetTasksOverviewAsync();
|
| 33 |
return StatusCode(result.StatusCode, result);
|
| 34 |
}
|
| 35 |
|
| 36 |
[HttpGet("project-progress")]
|
| 37 |
public async Task<ActionResult<Result<IEnumerable<ProjectProgressDto>>>> GetProjectProgress()
|
| 38 |
{
|
| 39 |
-
var result = await _dashboardService.GetProjectProgressAsync();
|
| 40 |
return StatusCode(result.StatusCode, result);
|
| 41 |
}
|
| 42 |
}
|
|
|
|
| 4 |
using System.Threading.Tasks;
|
| 5 |
using TaskTrackingSystem.Shared;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Dashboard;
|
| 7 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 8 |
|
| 9 |
namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
| 10 |
{
|
|
|
|
| 23 |
[HttpGet("summary")]
|
| 24 |
public async Task<ActionResult<Result<DashboardSummaryDto>>> GetSummary()
|
| 25 |
{
|
| 26 |
+
var result = await _dashboardService.GetSummaryAsync(User.GetRoleName(), User.GetUserId());
|
| 27 |
return StatusCode(result.StatusCode, result);
|
| 28 |
}
|
| 29 |
|
| 30 |
[HttpGet("tasks-overview")]
|
| 31 |
public async Task<ActionResult<Result<IEnumerable<TaskStatusOverviewDto>>>> GetTasksOverview()
|
| 32 |
{
|
| 33 |
+
var result = await _dashboardService.GetTasksOverviewAsync(User.GetRoleName(), User.GetUserId());
|
| 34 |
return StatusCode(result.StatusCode, result);
|
| 35 |
}
|
| 36 |
|
| 37 |
[HttpGet("project-progress")]
|
| 38 |
public async Task<ActionResult<Result<IEnumerable<ProjectProgressDto>>>> GetProjectProgress()
|
| 39 |
{
|
| 40 |
+
var result = await _dashboardService.GetProjectProgressAsync(User.GetRoleName(), User.GetUserId());
|
| 41 |
return StatusCode(result.StatusCode, result);
|
| 42 |
}
|
| 43 |
}
|
TaskTrackingSystem.WebApi/Features/Dashboard/DashboardService.cs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
|
|
|
| 2 |
using System.Collections.Generic;
|
| 3 |
using System.Linq;
|
| 4 |
using System.Threading.Tasks;
|
|
@@ -17,13 +18,18 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
|
| 17 |
_db = db;
|
| 18 |
}
|
| 19 |
|
| 20 |
-
public async Task<Result<DashboardSummaryDto>> GetSummaryAsync(
|
| 21 |
{
|
| 22 |
-
var
|
| 23 |
-
var
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
var summary = new DashboardSummaryDto
|
| 29 |
{
|
|
@@ -35,11 +41,9 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
|
| 35 |
return Result<DashboardSummaryDto>.Success(summary);
|
| 36 |
}
|
| 37 |
|
| 38 |
-
public async Task<Result<IEnumerable<TaskStatusOverviewDto>>> GetTasksOverviewAsync()
|
| 39 |
{
|
| 40 |
-
|
| 41 |
-
var groupedTasks = await _db.Tasks
|
| 42 |
-
.Where(t => t.IsDeleted != true)
|
| 43 |
.GroupBy(t => t.StatusId)
|
| 44 |
.Select(g => new
|
| 45 |
{
|
|
@@ -80,10 +84,9 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
|
| 80 |
return Result<IEnumerable<TaskStatusOverviewDto>>.Success(overview.OrderBy(o => o.StatusId));
|
| 81 |
}
|
| 82 |
|
| 83 |
-
public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync()
|
| 84 |
{
|
| 85 |
-
var activeProjects = await
|
| 86 |
-
.Where(p => p.IsDeleted != true)
|
| 87 |
.ToListAsync();
|
| 88 |
|
| 89 |
var progressList = new List<ProjectProgressDto>();
|
|
@@ -112,9 +115,38 @@ namespace TaskTrackingSystem.WebApi.Features.Dashboard
|
|
| 112 |
return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
|
| 113 |
}
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
{
|
| 117 |
-
|
|
|
|
| 118 |
}
|
| 119 |
}
|
| 120 |
}
|
|
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
| 2 |
+
using System;
|
| 3 |
using System.Collections.Generic;
|
| 4 |
using System.Linq;
|
| 5 |
using System.Threading.Tasks;
|
|
|
|
| 18 |
_db = db;
|
| 19 |
}
|
| 20 |
|
| 21 |
+
public async Task<Result<DashboardSummaryDto>> GetSummaryAsync(string roleName, long currentUserId)
|
| 22 |
{
|
| 23 |
+
var elevated = IsElevated(roleName);
|
| 24 |
+
var projects = BuildAccessibleProjectQuery(roleName, currentUserId);
|
| 25 |
+
var tasks = BuildAccessibleTaskQuery(roleName, currentUserId);
|
| 26 |
+
|
| 27 |
+
var totalUsers = elevated
|
| 28 |
+
? await _db.Users.CountAsync(u => !u.IsDeleted)
|
| 29 |
+
: await _db.Users.CountAsync(u => u.Id == currentUserId && !u.IsDeleted);
|
| 30 |
+
|
| 31 |
+
var activeProjectsCount = await projects.CountAsync();
|
| 32 |
+
var pendingTasksCount = await tasks.CountAsync(t => t.StatusId != 3);
|
| 33 |
|
| 34 |
var summary = new DashboardSummaryDto
|
| 35 |
{
|
|
|
|
| 41 |
return Result<DashboardSummaryDto>.Success(summary);
|
| 42 |
}
|
| 43 |
|
| 44 |
+
public async Task<Result<IEnumerable<TaskStatusOverviewDto>>> GetTasksOverviewAsync(string roleName, long currentUserId)
|
| 45 |
{
|
| 46 |
+
var groupedTasks = await BuildAccessibleTaskQuery(roleName, currentUserId)
|
|
|
|
|
|
|
| 47 |
.GroupBy(t => t.StatusId)
|
| 48 |
.Select(g => new
|
| 49 |
{
|
|
|
|
| 84 |
return Result<IEnumerable<TaskStatusOverviewDto>>.Success(overview.OrderBy(o => o.StatusId));
|
| 85 |
}
|
| 86 |
|
| 87 |
+
public async Task<Result<IEnumerable<ProjectProgressDto>>> GetProjectProgressAsync(string roleName, long currentUserId)
|
| 88 |
{
|
| 89 |
+
var activeProjects = await BuildAccessibleProjectQuery(roleName, currentUserId)
|
|
|
|
| 90 |
.ToListAsync();
|
| 91 |
|
| 92 |
var progressList = new List<ProjectProgressDto>();
|
|
|
|
| 115 |
return Result<IEnumerable<ProjectProgressDto>>.Success(progressList);
|
| 116 |
}
|
| 117 |
|
| 118 |
+
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
|
| 119 |
+
{
|
| 120 |
+
var query = _db.Tasks.Where(t => t.IsDeleted != true);
|
| 121 |
+
|
| 122 |
+
if (IsElevated(roleName))
|
| 123 |
+
{
|
| 124 |
+
return query;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return query.Where(t =>
|
| 128 |
+
t.AssignedTo == currentUserId ||
|
| 129 |
+
t.CreatedBy == currentUserId);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(string roleName, long currentUserId)
|
| 133 |
+
{
|
| 134 |
+
var query = _db.Projects.Where(p => p.IsDeleted != true);
|
| 135 |
+
|
| 136 |
+
if (IsElevated(roleName))
|
| 137 |
+
{
|
| 138 |
+
return query;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
return query.Where(p =>
|
| 142 |
+
p.CreatedById == currentUserId ||
|
| 143 |
+
p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
private static bool IsElevated(string roleName)
|
| 147 |
{
|
| 148 |
+
return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase) ||
|
| 149 |
+
roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
|
| 150 |
}
|
| 151 |
}
|
| 152 |
}
|
TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs
CHANGED
|
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|
| 4 |
using System.Security.Claims;
|
| 5 |
using System.Threading.Tasks;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Menu;
|
|
|
|
| 7 |
|
| 8 |
namespace TaskTrackingSystem.WebApi.Features.Menu
|
| 9 |
{
|
|
@@ -22,15 +23,24 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 22 |
[HttpGet]
|
| 23 |
public async Task<ActionResult<IEnumerable<MenuDto>>> GetMenus()
|
| 24 |
{
|
| 25 |
-
var
|
| 26 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
{
|
| 28 |
return Forbid();
|
| 29 |
}
|
| 30 |
|
| 31 |
-
var
|
| 32 |
-
return Ok(
|
| 33 |
}
|
|
|
|
|
|
|
| 34 |
[HttpGet("all")]
|
| 35 |
public async Task<ActionResult<IEnumerable<MenuAdminDto>>> GetAllMenus()
|
| 36 |
{
|
|
|
|
| 4 |
using System.Security.Claims;
|
| 5 |
using System.Threading.Tasks;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Menu;
|
| 7 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 8 |
|
| 9 |
namespace TaskTrackingSystem.WebApi.Features.Menu
|
| 10 |
{
|
|
|
|
| 23 |
[HttpGet]
|
| 24 |
public async Task<ActionResult<IEnumerable<MenuDto>>> GetMenus()
|
| 25 |
{
|
| 26 |
+
var roleId = User.GetRoleId();
|
| 27 |
+
if (roleId > 0)
|
| 28 |
+
{
|
| 29 |
+
var roleMenus = await _menuService.GetMenusByRoleIdAsync(roleId);
|
| 30 |
+
return Ok(roleMenus);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
var roleName = User.GetRoleName();
|
| 34 |
+
if (string.IsNullOrWhiteSpace(roleName))
|
| 35 |
{
|
| 36 |
return Forbid();
|
| 37 |
}
|
| 38 |
|
| 39 |
+
var roleMenusByName = await _menuService.GetMenusByRoleNameAsync(roleName);
|
| 40 |
+
return Ok(roleMenusByName);
|
| 41 |
}
|
| 42 |
+
|
| 43 |
+
[Authorize(Roles = "Admin")]
|
| 44 |
[HttpGet("all")]
|
| 45 |
public async Task<ActionResult<IEnumerable<MenuAdminDto>>> GetAllMenus()
|
| 46 |
{
|
TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs
CHANGED
|
@@ -17,17 +17,43 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 17 |
_db = db;
|
| 18 |
}
|
| 19 |
|
| 20 |
-
public async Task<List<MenuDto>>
|
| 21 |
{
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
.OrderBy(m => m.OrderNo)
|
| 32 |
.Select(m => new MenuDto
|
| 33 |
{
|
|
@@ -40,19 +66,131 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
|
|
| 40 |
})
|
| 41 |
.ToListAsync();
|
| 42 |
|
| 43 |
-
//
|
| 44 |
-
var
|
| 45 |
-
var childMenus = menus.Where(m => !string.IsNullOrEmpty(m.ParentCode) && m.ParentCode != "0").ToList();
|
| 46 |
|
| 47 |
-
|
| 48 |
{
|
| 49 |
-
|
| 50 |
-
.Where(c => c.ParentCode == parent.MenuCode)
|
| 51 |
-
.OrderBy(c => c.OrderNo)
|
| 52 |
-
.ToList();
|
| 53 |
}
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
public async Task<List<MenuAdminDto>> GetAllMenusAsync()
|
| 58 |
{
|
|
|
|
| 17 |
_db = db;
|
| 18 |
}
|
| 19 |
|
| 20 |
+
public async Task<List<MenuDto>> GetMenusByRoleIdAsync(long roleId)
|
| 21 |
{
|
| 22 |
+
var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
|
| 23 |
+
if (role == null)
|
| 24 |
+
{
|
| 25 |
+
return new List<MenuDto>();
|
| 26 |
+
}
|
| 27 |
|
| 28 |
+
return await GetMenusForRoleAsync(role.Id, role.Name);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public async Task<List<MenuDto>> GetMenusByRoleNameAsync(string roleName)
|
| 32 |
+
{
|
| 33 |
+
if (string.IsNullOrWhiteSpace(roleName))
|
| 34 |
+
{
|
| 35 |
+
return new List<MenuDto>();
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true);
|
| 39 |
+
if (role == null)
|
| 40 |
+
{
|
| 41 |
+
return new List<MenuDto>();
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return await GetMenusForRoleAsync(role.Id, role.Name);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
private async Task<List<MenuDto>> GetMenusForRoleAsync(long roleId, string roleName)
|
| 48 |
+
{
|
| 49 |
+
var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
|
| 50 |
+
if (role == null)
|
| 51 |
+
{
|
| 52 |
+
return new List<MenuDto>();
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
var allVisibleMenus = await _db.MenuAdmins
|
| 56 |
+
.Where(m => m.Visible && m.DelFlag == 0)
|
| 57 |
.OrderBy(m => m.OrderNo)
|
| 58 |
.Select(m => new MenuDto
|
| 59 |
{
|
|
|
|
| 66 |
})
|
| 67 |
.ToListAsync();
|
| 68 |
|
| 69 |
+
// Keep compatibility with roles stored by either RoleId or the older RoleCode link.
|
| 70 |
+
var allowedMenuCodes = await GetAllowedMenuCodesAsync(role.Id, role.Name);
|
|
|
|
| 71 |
|
| 72 |
+
if (allowedMenuCodes.Count == 0)
|
| 73 |
{
|
| 74 |
+
return new List<MenuDto>();
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
+
var visibleMenuLookup = allVisibleMenus
|
| 78 |
+
.GroupBy(m => m.MenuCode, StringComparer.OrdinalIgnoreCase)
|
| 79 |
+
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
| 80 |
+
|
| 81 |
+
var expandedMenuCodes = ExpandWithAncestors(allowedMenuCodes, visibleMenuLookup);
|
| 82 |
+
var menus = allVisibleMenus
|
| 83 |
+
.Where(m => expandedMenuCodes.Contains(m.MenuCode))
|
| 84 |
+
.OrderBy(m => m.OrderNo)
|
| 85 |
+
.ToList();
|
| 86 |
+
|
| 87 |
+
return BuildHierarchy(menus);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
private static List<MenuDto> BuildHierarchy(List<MenuDto> menus)
|
| 91 |
+
{
|
| 92 |
+
if (menus.Count == 0)
|
| 93 |
+
{
|
| 94 |
+
return new List<MenuDto>();
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
var menuLookup = menus
|
| 98 |
+
.GroupBy(m => m.MenuCode, StringComparer.OrdinalIgnoreCase)
|
| 99 |
+
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
| 100 |
+
|
| 101 |
+
foreach (var menu in menuLookup.Values)
|
| 102 |
+
{
|
| 103 |
+
menu.SubMenus = new List<MenuDto>();
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
var roots = new List<MenuDto>();
|
| 107 |
+
|
| 108 |
+
foreach (var menu in menuLookup.Values.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName))
|
| 109 |
+
{
|
| 110 |
+
var parentCode = NormalizeMenuCode(menu.ParentCode);
|
| 111 |
+
if (string.IsNullOrWhiteSpace(parentCode) ||
|
| 112 |
+
parentCode == "0" ||
|
| 113 |
+
!menuLookup.TryGetValue(parentCode, out var parent))
|
| 114 |
+
{
|
| 115 |
+
roots.Add(menu);
|
| 116 |
+
continue;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
parent.SubMenus.Add(menu);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
SortMenuTree(roots);
|
| 123 |
+
return roots;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
private async Task<HashSet<string>> GetAllowedMenuCodesAsync(long roleId, string roleName)
|
| 127 |
+
{
|
| 128 |
+
var allowedMenuCodes = await _db.RoleMenus
|
| 129 |
+
.Where(rm => rm.RoleId == roleId && rm.DelFlag == 0)
|
| 130 |
+
.Select(rm => rm.MenuCode)
|
| 131 |
+
.ToListAsync();
|
| 132 |
+
|
| 133 |
+
if (allowedMenuCodes.Count > 0)
|
| 134 |
+
{
|
| 135 |
+
return allowedMenuCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if (!string.IsNullOrWhiteSpace(roleName))
|
| 139 |
+
{
|
| 140 |
+
allowedMenuCodes = await _db.RoleMenus
|
| 141 |
+
.Where(rm => rm.RoleCode == roleName && rm.DelFlag == 0)
|
| 142 |
+
.Select(rm => rm.MenuCode)
|
| 143 |
+
.ToListAsync();
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return allowedMenuCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
private static HashSet<string> ExpandWithAncestors(IEnumerable<string> menuCodes, IReadOnlyDictionary<string, MenuDto> menuLookup)
|
| 150 |
+
{
|
| 151 |
+
var expanded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
| 152 |
+
|
| 153 |
+
foreach (var menuCode in menuCodes.Where(code => !string.IsNullOrWhiteSpace(code)))
|
| 154 |
+
{
|
| 155 |
+
var currentCode = menuCode.Trim();
|
| 156 |
+
|
| 157 |
+
while (!string.IsNullOrWhiteSpace(currentCode) && expanded.Add(currentCode))
|
| 158 |
+
{
|
| 159 |
+
if (!menuLookup.TryGetValue(currentCode, out var currentMenu))
|
| 160 |
+
{
|
| 161 |
+
break;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
currentCode = NormalizeMenuCode(currentMenu.ParentCode);
|
| 165 |
+
if (string.IsNullOrWhiteSpace(currentCode) || currentCode == "0")
|
| 166 |
+
{
|
| 167 |
+
break;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
return expanded;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
private static void SortMenuTree(IList<MenuDto> menus)
|
| 176 |
+
{
|
| 177 |
+
var ordered = menus.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase).ToList();
|
| 178 |
+
menus.Clear();
|
| 179 |
+
|
| 180 |
+
foreach (var menu in ordered)
|
| 181 |
+
{
|
| 182 |
+
if (menu.SubMenus.Count > 0)
|
| 183 |
+
{
|
| 184 |
+
SortMenuTree(menu.SubMenus);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
menus.Add(menu);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
private static string NormalizeMenuCode(string? code)
|
| 192 |
+
{
|
| 193 |
+
return string.IsNullOrWhiteSpace(code) ? string.Empty : code.Trim();
|
| 194 |
}
|
| 195 |
public async Task<List<MenuAdminDto>> GetAllMenusAsync()
|
| 196 |
{
|
TaskTrackingSystem.WebApi/Features/Project/ProjectController.cs
CHANGED
|
@@ -4,6 +4,7 @@ using TaskTrackingSystem.Shared;
|
|
| 4 |
using TaskTrackingSystem.Shared.Models.User;
|
| 5 |
using TaskTrackingSystem.Shared.Models.Task;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Project;
|
|
|
|
| 7 |
|
| 8 |
namespace TaskTrackingSystem.WebApi.Features.Project
|
| 9 |
{
|
|
@@ -22,14 +23,14 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 22 |
[HttpGet]
|
| 23 |
public async Task<ActionResult<IEnumerable<ProjectDto>>> GetProjects()
|
| 24 |
{
|
| 25 |
-
var projects = await _projectService.GetAllProjectsAsync();
|
| 26 |
return Ok(projects);
|
| 27 |
}
|
| 28 |
|
| 29 |
[HttpGet("{id}")]
|
| 30 |
public async Task<ActionResult<ProjectDto>> GetProject(long id)
|
| 31 |
{
|
| 32 |
-
var project = await _projectService.GetProjectByIdAsync(id);
|
| 33 |
if (project == null)
|
| 34 |
{
|
| 35 |
return NotFound(new { message = $"Project with ID {id} not found." });
|
|
@@ -38,22 +39,25 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 38 |
}
|
| 39 |
|
| 40 |
[HttpPost]
|
|
|
|
| 41 |
public async Task<ActionResult<Result<ProjectDto>>> CreateProject([FromBody] CreateProjectDto createProjectDto)
|
| 42 |
{
|
| 43 |
-
long? currentUserId =
|
| 44 |
var result = await _projectService.CreateProjectAsync(createProjectDto, currentUserId);
|
| 45 |
return StatusCode(result.StatusCode, result);
|
| 46 |
}
|
| 47 |
|
| 48 |
[HttpPut("{id}")]
|
|
|
|
| 49 |
public async Task<ActionResult<Result>> UpdateProject(long id, [FromBody] UpdateProjectDto updateProjectDto)
|
| 50 |
{
|
| 51 |
-
long? currentUserId =
|
| 52 |
var result = await _projectService.UpdateProjectAsync(id, updateProjectDto, currentUserId);
|
| 53 |
return StatusCode(result.StatusCode, result);
|
| 54 |
}
|
| 55 |
|
| 56 |
[HttpDelete("{id}")]
|
|
|
|
| 57 |
public async Task<ActionResult<Result>> DeleteProject(long id)
|
| 58 |
{
|
| 59 |
var result = await _projectService.SoftDeleteProjectAsync(id);
|
|
@@ -63,14 +67,15 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 63 |
[HttpGet("{id}/members")]
|
| 64 |
public async Task<ActionResult<Result<IEnumerable<UserDto>>>> GetProjectMembers(long id)
|
| 65 |
{
|
| 66 |
-
var result = await _projectService.GetProjectMembersAsync(id);
|
| 67 |
return StatusCode(result.StatusCode, result);
|
| 68 |
}
|
| 69 |
|
| 70 |
[HttpPost("{id}/members")]
|
|
|
|
| 71 |
public async Task<IActionResult> AssignProjectMembers(long id, [FromBody] AssignMembersDto dto)
|
| 72 |
{
|
| 73 |
-
var result = await _projectService.AssignMembersToProjectAsync(id, dto);
|
| 74 |
if (!result.IsSuccess)
|
| 75 |
{
|
| 76 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
|
@@ -79,9 +84,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 79 |
}
|
| 80 |
|
| 81 |
[HttpDelete("{id}/members/{userId}")]
|
|
|
|
| 82 |
public async Task<IActionResult> RemoveProjectMember(long id, long userId)
|
| 83 |
{
|
| 84 |
-
var result = await _projectService.RemoveMemberFromProjectAsync(id, userId);
|
| 85 |
if (!result.IsSuccess)
|
| 86 |
{
|
| 87 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
|
@@ -92,7 +98,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 92 |
[HttpGet("{id}/tasks")]
|
| 93 |
public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetProjectTasks(long id)
|
| 94 |
{
|
| 95 |
-
var result = await _projectService.GetProjectTasksAsync(id);
|
| 96 |
return StatusCode(result.StatusCode, result);
|
| 97 |
}
|
| 98 |
}
|
|
|
|
| 4 |
using TaskTrackingSystem.Shared.Models.User;
|
| 5 |
using TaskTrackingSystem.Shared.Models.Task;
|
| 6 |
using TaskTrackingSystem.Shared.Models.Project;
|
| 7 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 8 |
|
| 9 |
namespace TaskTrackingSystem.WebApi.Features.Project
|
| 10 |
{
|
|
|
|
| 23 |
[HttpGet]
|
| 24 |
public async Task<ActionResult<IEnumerable<ProjectDto>>> GetProjects()
|
| 25 |
{
|
| 26 |
+
var projects = await _projectService.GetAllProjectsAsync(User.GetRoleName(), User.GetUserId());
|
| 27 |
return Ok(projects);
|
| 28 |
}
|
| 29 |
|
| 30 |
[HttpGet("{id}")]
|
| 31 |
public async Task<ActionResult<ProjectDto>> GetProject(long id)
|
| 32 |
{
|
| 33 |
+
var project = await _projectService.GetProjectByIdAsync(id, User.GetRoleName(), User.GetUserId());
|
| 34 |
if (project == null)
|
| 35 |
{
|
| 36 |
return NotFound(new { message = $"Project with ID {id} not found." });
|
|
|
|
| 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);
|
|
|
|
| 67 |
[HttpGet("{id}/members")]
|
| 68 |
public async Task<ActionResult<Result<IEnumerable<UserDto>>>> GetProjectMembers(long id)
|
| 69 |
{
|
| 70 |
+
var result = await _projectService.GetProjectMembersAsync(id, User.GetRoleName(), User.GetUserId());
|
| 71 |
return StatusCode(result.StatusCode, result);
|
| 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 |
{
|
| 81 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
|
|
|
| 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 |
{
|
| 93 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
|
|
|
| 98 |
[HttpGet("{id}/tasks")]
|
| 99 |
public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetProjectTasks(long id)
|
| 100 |
{
|
| 101 |
+
var result = await _projectService.GetProjectTasksAsync(id, User.GetRoleName(), User.GetUserId());
|
| 102 |
return StatusCode(result.StatusCode, result);
|
| 103 |
}
|
| 104 |
}
|
TaskTrackingSystem.WebApi/Features/Project/ProjectService.cs
CHANGED
|
@@ -20,10 +20,9 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 20 |
_db = db;
|
| 21 |
}
|
| 22 |
|
| 23 |
-
public async Task<IEnumerable<ProjectDto>> GetAllProjectsAsync()
|
| 24 |
{
|
| 25 |
-
return await
|
| 26 |
-
.Where(p => p.IsDeleted != true)
|
| 27 |
.OrderBy(p => p.EndDate)
|
| 28 |
.ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow)
|
| 29 |
.Select(p => new ProjectDto
|
|
@@ -39,10 +38,10 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 39 |
}).ToListAsync();
|
| 40 |
}
|
| 41 |
|
| 42 |
-
public async Task<ProjectDto?> GetProjectByIdAsync(long id)
|
| 43 |
{
|
| 44 |
-
var project = await
|
| 45 |
-
.FirstOrDefaultAsync(p => p.Id == id
|
| 46 |
|
| 47 |
if (project == null) return null;
|
| 48 |
|
|
@@ -131,12 +130,11 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 131 |
return Result.Success(200);
|
| 132 |
}
|
| 133 |
|
| 134 |
-
public async Task<Result<IEnumerable<UserDto>>> GetProjectMembersAsync(long projectId)
|
| 135 |
{
|
| 136 |
-
|
| 137 |
-
if (!projectExists)
|
| 138 |
{
|
| 139 |
-
return Result<IEnumerable<UserDto>>.Failure(
|
| 140 |
}
|
| 141 |
|
| 142 |
var members = await _db.ProjectMembers
|
|
@@ -161,7 +159,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 161 |
return Result<IEnumerable<UserDto>>.Success(members);
|
| 162 |
}
|
| 163 |
|
| 164 |
-
public async Task<Result> AssignMembersToProjectAsync(long projectId, AssignMembersDto dto)
|
| 165 |
{
|
| 166 |
var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
|
| 167 |
if (!projectExists)
|
|
@@ -214,7 +212,7 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 214 |
return Result.Success(200);
|
| 215 |
}
|
| 216 |
|
| 217 |
-
public async Task<Result> RemoveMemberFromProjectAsync(long projectId, long userId)
|
| 218 |
{
|
| 219 |
var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
|
| 220 |
if (!projectExists)
|
|
@@ -236,12 +234,11 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 236 |
return Result.Success(204); // No Content success
|
| 237 |
}
|
| 238 |
|
| 239 |
-
public async Task<Result<IEnumerable<TaskDto>>> GetProjectTasksAsync(long projectId)
|
| 240 |
{
|
| 241 |
-
|
| 242 |
-
if (!projectExists)
|
| 243 |
{
|
| 244 |
-
return Result<IEnumerable<TaskDto>>.Failure(
|
| 245 |
}
|
| 246 |
|
| 247 |
var tasks = await _db.Tasks
|
|
@@ -269,5 +266,38 @@ namespace TaskTrackingSystem.WebApi.Features.Project
|
|
| 269 |
|
| 270 |
return Result<IEnumerable<TaskDto>>.Success(tasks);
|
| 271 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
}
|
| 273 |
}
|
|
|
|
| 20 |
_db = db;
|
| 21 |
}
|
| 22 |
|
| 23 |
+
public async Task<IEnumerable<ProjectDto>> GetAllProjectsAsync(string roleName, long currentUserId)
|
| 24 |
{
|
| 25 |
+
return await BuildAccessibleProjectQuery(roleName, currentUserId)
|
|
|
|
| 26 |
.OrderBy(p => p.EndDate)
|
| 27 |
.ThenByDescending(p => p.CreatedAt ?? DateTime.UtcNow)
|
| 28 |
.Select(p => new ProjectDto
|
|
|
|
| 38 |
}).ToListAsync();
|
| 39 |
}
|
| 40 |
|
| 41 |
+
public async Task<ProjectDto?> GetProjectByIdAsync(long id, string roleName, long currentUserId)
|
| 42 |
{
|
| 43 |
+
var project = await BuildAccessibleProjectQuery(roleName, currentUserId)
|
| 44 |
+
.FirstOrDefaultAsync(p => p.Id == id);
|
| 45 |
|
| 46 |
if (project == null) return null;
|
| 47 |
|
|
|
|
| 130 |
return Result.Success(200);
|
| 131 |
}
|
| 132 |
|
| 133 |
+
public async Task<Result<IEnumerable<UserDto>>> GetProjectMembersAsync(long projectId, string roleName, long currentUserId)
|
| 134 |
{
|
| 135 |
+
if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
|
|
|
|
| 136 |
{
|
| 137 |
+
return Result<IEnumerable<UserDto>>.Failure("You do not have access to this project.", 403);
|
| 138 |
}
|
| 139 |
|
| 140 |
var members = await _db.ProjectMembers
|
|
|
|
| 159 |
return Result<IEnumerable<UserDto>>.Success(members);
|
| 160 |
}
|
| 161 |
|
| 162 |
+
public async Task<Result> AssignMembersToProjectAsync(long projectId, AssignMembersDto dto, long? currentUserId = null)
|
| 163 |
{
|
| 164 |
var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
|
| 165 |
if (!projectExists)
|
|
|
|
| 212 |
return Result.Success(200);
|
| 213 |
}
|
| 214 |
|
| 215 |
+
public async Task<Result> RemoveMemberFromProjectAsync(long projectId, long userId, long? currentUserId = null)
|
| 216 |
{
|
| 217 |
var projectExists = await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
|
| 218 |
if (!projectExists)
|
|
|
|
| 234 |
return Result.Success(204); // No Content success
|
| 235 |
}
|
| 236 |
|
| 237 |
+
public async Task<Result<IEnumerable<TaskDto>>> GetProjectTasksAsync(long projectId, string roleName, long currentUserId)
|
| 238 |
{
|
| 239 |
+
if (!await IsProjectAccessibleAsync(projectId, roleName, currentUserId))
|
|
|
|
| 240 |
{
|
| 241 |
+
return Result<IEnumerable<TaskDto>>.Failure("You do not have access to this project.", 403);
|
| 242 |
}
|
| 243 |
|
| 244 |
var tasks = await _db.Tasks
|
|
|
|
| 266 |
|
| 267 |
return Result<IEnumerable<TaskDto>>.Success(tasks);
|
| 268 |
}
|
| 269 |
+
|
| 270 |
+
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Project> BuildAccessibleProjectQuery(string roleName, long currentUserId)
|
| 271 |
+
{
|
| 272 |
+
var query = _db.Projects.Where(p => p.IsDeleted != true);
|
| 273 |
+
|
| 274 |
+
if (IsElevated(roleName))
|
| 275 |
+
{
|
| 276 |
+
return query;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
return query.Where(p =>
|
| 280 |
+
p.CreatedById == currentUserId ||
|
| 281 |
+
p.ProjectMembers.Any(pm => pm.UserId == currentUserId));
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
private async Task<bool> IsProjectAccessibleAsync(long projectId, string roleName, long currentUserId)
|
| 285 |
+
{
|
| 286 |
+
if (IsElevated(roleName))
|
| 287 |
+
{
|
| 288 |
+
return await _db.Projects.AnyAsync(p => p.Id == projectId && p.IsDeleted != true);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
return await _db.Projects.AnyAsync(p =>
|
| 292 |
+
p.Id == projectId &&
|
| 293 |
+
p.IsDeleted != true &&
|
| 294 |
+
(p.CreatedById == currentUserId || p.ProjectMembers.Any(pm => pm.UserId == currentUserId)));
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
private static bool IsElevated(string roleName)
|
| 298 |
+
{
|
| 299 |
+
return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase) ||
|
| 300 |
+
roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
|
| 301 |
+
}
|
| 302 |
}
|
| 303 |
}
|
TaskTrackingSystem.WebApi/Features/Role/RoleController.cs
CHANGED
|
@@ -7,7 +7,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 7 |
{
|
| 8 |
[Route("api/[controller]")]
|
| 9 |
[ApiController]
|
| 10 |
-
[Authorize]
|
| 11 |
public class RoleController : ControllerBase
|
| 12 |
{
|
| 13 |
private readonly RoleService _roleService;
|
|
@@ -36,7 +36,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 36 |
}
|
| 37 |
|
| 38 |
[HttpPost]
|
| 39 |
-
[Authorize(Roles = "Admin")]
|
| 40 |
public async Task<ActionResult<Result<RoleDto>>> CreateRole([FromBody] CreateRoleDto createRoleDto)
|
| 41 |
{
|
| 42 |
long? currentUserId = null;
|
|
@@ -45,7 +44,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 45 |
}
|
| 46 |
|
| 47 |
[HttpPut("{id}")]
|
| 48 |
-
[Authorize(Roles = "Admin")]
|
| 49 |
public async Task<ActionResult<Result>> UpdateRole(long id, [FromBody] UpdateRoleDto updateRoleDto)
|
| 50 |
{
|
| 51 |
long? currentUserId = null;
|
|
@@ -54,7 +52,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 54 |
}
|
| 55 |
|
| 56 |
[HttpDelete("{id}")]
|
| 57 |
-
[Authorize(Roles = "Admin")]
|
| 58 |
public async Task<ActionResult<Result>> DeleteRole(long id)
|
| 59 |
{
|
| 60 |
var result = await _roleService.SoftDeleteRoleAsync(id);
|
|
@@ -63,7 +60,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 63 |
|
| 64 |
|
| 65 |
[HttpGet("{id}/menus")]
|
| 66 |
-
[Authorize(Roles = "Admin")]
|
| 67 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedMenus(long id)
|
| 68 |
{
|
| 69 |
var result = await _roleService.GetAssignedMenusByRoleIdAsync(id);
|
|
|
|
| 7 |
{
|
| 8 |
[Route("api/[controller]")]
|
| 9 |
[ApiController]
|
| 10 |
+
[Authorize(Roles = "Admin")]
|
| 11 |
public class RoleController : ControllerBase
|
| 12 |
{
|
| 13 |
private readonly RoleService _roleService;
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
[HttpPost]
|
|
|
|
| 39 |
public async Task<ActionResult<Result<RoleDto>>> CreateRole([FromBody] CreateRoleDto createRoleDto)
|
| 40 |
{
|
| 41 |
long? currentUserId = null;
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
[HttpPut("{id}")]
|
|
|
|
| 47 |
public async Task<ActionResult<Result>> UpdateRole(long id, [FromBody] UpdateRoleDto updateRoleDto)
|
| 48 |
{
|
| 49 |
long? currentUserId = null;
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
[HttpDelete("{id}")]
|
|
|
|
| 55 |
public async Task<ActionResult<Result>> DeleteRole(long id)
|
| 56 |
{
|
| 57 |
var result = await _roleService.SoftDeleteRoleAsync(id);
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
[HttpGet("{id}/menus")]
|
|
|
|
| 63 |
public async Task<ActionResult<Result<List<string>>>> GetAssignedMenus(long id)
|
| 64 |
{
|
| 65 |
var result = await _roleService.GetAssignedMenusByRoleIdAsync(id);
|
TaskTrackingSystem.WebApi/Features/Role/RoleService.cs
CHANGED
|
@@ -27,7 +27,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 27 |
Id = r.Id,
|
| 28 |
Name = r.Name,
|
| 29 |
Description = r.Description,
|
| 30 |
-
CreatedAt =
|
| 31 |
}).ToListAsync();
|
| 32 |
}
|
| 33 |
|
|
@@ -43,7 +43,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 43 |
Id = role.Id,
|
| 44 |
Name = role.Name,
|
| 45 |
Description = role.Description,
|
| 46 |
-
CreatedAt =
|
| 47 |
};
|
| 48 |
}
|
| 49 |
|
|
@@ -60,6 +60,15 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 60 |
return Result<RoleDto>.Failure("Role name is already taken.", 400);
|
| 61 |
}
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
var role = new TaskTrackingSystem.Database.AppDbContextModels.Role
|
| 64 |
{
|
| 65 |
Name = dto.Name,
|
|
@@ -71,22 +80,10 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 71 |
_db.Roles.Add(role);
|
| 72 |
await _db.SaveChangesAsync();
|
| 73 |
|
| 74 |
-
|
| 75 |
-
if (
|
| 76 |
{
|
| 77 |
-
|
| 78 |
-
foreach (var menuCode in dto.MenuCodes)
|
| 79 |
-
{
|
| 80 |
-
_db.RoleMenus.Add(new RoleMenu
|
| 81 |
-
{
|
| 82 |
-
RoleMenuId = $"RM_{role.Name}_{i++}_{DateTime.UtcNow.Ticks}",
|
| 83 |
-
RoleCode = role.Name,
|
| 84 |
-
MenuCode = menuCode,
|
| 85 |
-
DelFlag = 0,
|
| 86 |
-
CreatedDateTime = DateTime.UtcNow
|
| 87 |
-
});
|
| 88 |
-
}
|
| 89 |
-
await _db.SaveChangesAsync();
|
| 90 |
}
|
| 91 |
|
| 92 |
var resultDto = new RoleDto
|
|
@@ -116,11 +113,21 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 116 |
return Result.Failure("Role name is already taken by another role.", 400);
|
| 117 |
}
|
| 118 |
|
|
|
|
| 119 |
role.Name = dto.Name;
|
| 120 |
role.Description = dto.Description;
|
| 121 |
role.UpdatedAt = DateTime.UtcNow;
|
| 122 |
role.UpdatedBy = currentUserId;
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
_db.Roles.Update(role);
|
| 125 |
await _db.SaveChangesAsync();
|
| 126 |
return Result.Success(200);
|
|
@@ -147,10 +154,18 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 147 |
}
|
| 148 |
|
| 149 |
var menuCodes = await _db.RoleMenus
|
| 150 |
-
.Where(rm => rm.
|
| 151 |
.Select(rm => rm.MenuCode)
|
| 152 |
.ToListAsync();
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
return Result<List<string>>.Success(menuCodes);
|
| 155 |
}
|
| 156 |
|
|
@@ -167,9 +182,15 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 167 |
return Result.Failure("MenuCodes cannot be null", 400);
|
| 168 |
}
|
| 169 |
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
var existingRoleMenus = await _db.RoleMenus
|
| 172 |
-
.Where(rm => rm.RoleCode == role.Name)
|
| 173 |
.ToListAsync();
|
| 174 |
|
| 175 |
if (existingRoleMenus.Any())
|
|
@@ -183,7 +204,8 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 183 |
{
|
| 184 |
_db.RoleMenus.Add(new RoleMenu
|
| 185 |
{
|
| 186 |
-
RoleMenuId = $"RM_{role.
|
|
|
|
| 187 |
RoleCode = role.Name,
|
| 188 |
MenuCode = menuCode,
|
| 189 |
DelFlag = 0,
|
|
@@ -194,5 +216,35 @@ namespace TaskTrackingSystem.WebApi.Features.Role
|
|
| 194 |
await _db.SaveChangesAsync();
|
| 195 |
return Result.Success(200);
|
| 196 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
}
|
| 198 |
}
|
|
|
|
| 27 |
Id = r.Id,
|
| 28 |
Name = r.Name,
|
| 29 |
Description = r.Description,
|
| 30 |
+
CreatedAt = r.CreatedAt ?? DateTime.UtcNow
|
| 31 |
}).ToListAsync();
|
| 32 |
}
|
| 33 |
|
|
|
|
| 43 |
Id = role.Id,
|
| 44 |
Name = role.Name,
|
| 45 |
Description = role.Description,
|
| 46 |
+
CreatedAt = role.CreatedAt ?? DateTime.UtcNow
|
| 47 |
};
|
| 48 |
}
|
| 49 |
|
|
|
|
| 60 |
return Result<RoleDto>.Failure("Role name is already taken.", 400);
|
| 61 |
}
|
| 62 |
|
| 63 |
+
if (dto.MenuCodes != null && dto.MenuCodes.Any())
|
| 64 |
+
{
|
| 65 |
+
var validCodesResult = await ValidatePermissionCodesAsync(dto.MenuCodes);
|
| 66 |
+
if (!validCodesResult.IsSuccess)
|
| 67 |
+
{
|
| 68 |
+
return Result<RoleDto>.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, validCodesResult.StatusCode);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
var role = new TaskTrackingSystem.Database.AppDbContextModels.Role
|
| 73 |
{
|
| 74 |
Name = dto.Name,
|
|
|
|
| 80 |
_db.Roles.Add(role);
|
| 81 |
await _db.SaveChangesAsync();
|
| 82 |
|
| 83 |
+
var assignResult = await AssignMenusToRoleAsync(role.Id, new AssignMenusDto { MenuCodes = dto.MenuCodes ?? new List<string>() });
|
| 84 |
+
if (!assignResult.IsSuccess)
|
| 85 |
{
|
| 86 |
+
return Result<RoleDto>.Failure(assignResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, assignResult.StatusCode);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
|
| 89 |
var resultDto = new RoleDto
|
|
|
|
| 113 |
return Result.Failure("Role name is already taken by another role.", 400);
|
| 114 |
}
|
| 115 |
|
| 116 |
+
var oldName = role.Name;
|
| 117 |
role.Name = dto.Name;
|
| 118 |
role.Description = dto.Description;
|
| 119 |
role.UpdatedAt = DateTime.UtcNow;
|
| 120 |
role.UpdatedBy = currentUserId;
|
| 121 |
|
| 122 |
+
var relatedRoleMenus = await _db.RoleMenus
|
| 123 |
+
.Where(rm => rm.RoleId == role.Id || rm.RoleCode == oldName)
|
| 124 |
+
.ToListAsync();
|
| 125 |
+
|
| 126 |
+
foreach (var roleMenu in relatedRoleMenus)
|
| 127 |
+
{
|
| 128 |
+
roleMenu.RoleCode = dto.Name;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
_db.Roles.Update(role);
|
| 132 |
await _db.SaveChangesAsync();
|
| 133 |
return Result.Success(200);
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
var menuCodes = await _db.RoleMenus
|
| 157 |
+
.Where(rm => rm.RoleId == role.Id && rm.DelFlag == 0)
|
| 158 |
.Select(rm => rm.MenuCode)
|
| 159 |
.ToListAsync();
|
| 160 |
|
| 161 |
+
if (!menuCodes.Any())
|
| 162 |
+
{
|
| 163 |
+
menuCodes = await _db.RoleMenus
|
| 164 |
+
.Where(rm => rm.RoleCode == role.Name && rm.DelFlag == 0)
|
| 165 |
+
.Select(rm => rm.MenuCode)
|
| 166 |
+
.ToListAsync();
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
return Result<List<string>>.Success(menuCodes);
|
| 170 |
}
|
| 171 |
|
|
|
|
| 182 |
return Result.Failure("MenuCodes cannot be null", 400);
|
| 183 |
}
|
| 184 |
|
| 185 |
+
var validCodesResult = await ValidatePermissionCodesAsync(dto.MenuCodes);
|
| 186 |
+
if (!validCodesResult.IsSuccess)
|
| 187 |
+
{
|
| 188 |
+
return Result.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToUpdateRole, validCodesResult.StatusCode);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Remove existing role menus matching either the new RoleId link or the old RoleCode link.
|
| 192 |
var existingRoleMenus = await _db.RoleMenus
|
| 193 |
+
.Where(rm => rm.RoleId == role.Id || rm.RoleCode == role.Name)
|
| 194 |
.ToListAsync();
|
| 195 |
|
| 196 |
if (existingRoleMenus.Any())
|
|
|
|
| 204 |
{
|
| 205 |
_db.RoleMenus.Add(new RoleMenu
|
| 206 |
{
|
| 207 |
+
RoleMenuId = $"RM_{role.Id}_{i++}_{DateTime.UtcNow.Ticks}",
|
| 208 |
+
RoleId = role.Id,
|
| 209 |
RoleCode = role.Name,
|
| 210 |
MenuCode = menuCode,
|
| 211 |
DelFlag = 0,
|
|
|
|
| 216 |
await _db.SaveChangesAsync();
|
| 217 |
return Result.Success(200);
|
| 218 |
}
|
| 219 |
+
|
| 220 |
+
private async Task<Result> ValidatePermissionCodesAsync(IEnumerable<string> menuCodes)
|
| 221 |
+
{
|
| 222 |
+
var validMenuCodes = await _db.MenuAdmins
|
| 223 |
+
.Where(m => m.DelFlag == 0)
|
| 224 |
+
.Select(m => m.MenuCode)
|
| 225 |
+
.ToListAsync();
|
| 226 |
+
|
| 227 |
+
var validActionCodes = await _db.MenuAdminDetails
|
| 228 |
+
.Where(d => d.DelFlag == 0)
|
| 229 |
+
.Select(d => d.MenuDetailCode)
|
| 230 |
+
.ToListAsync();
|
| 231 |
+
|
| 232 |
+
var validCodes = validMenuCodes
|
| 233 |
+
.Concat(validActionCodes)
|
| 234 |
+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
| 235 |
+
|
| 236 |
+
var invalidCodes = menuCodes
|
| 237 |
+
.Where(code => !string.IsNullOrWhiteSpace(code))
|
| 238 |
+
.Where(code => !validCodes.Contains(code))
|
| 239 |
+
.Distinct(StringComparer.OrdinalIgnoreCase)
|
| 240 |
+
.ToList();
|
| 241 |
+
|
| 242 |
+
if (invalidCodes.Any())
|
| 243 |
+
{
|
| 244 |
+
return Result.Failure(ResultMessages.InvalidPermissionIds(string.Join(", ", invalidCodes)), 400);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
return Result.Success(200);
|
| 248 |
+
}
|
| 249 |
}
|
| 250 |
}
|
TaskTrackingSystem.WebApi/Features/Task/TaskController.cs
CHANGED
|
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|
| 4 |
using TaskTrackingSystem.Shared;
|
| 5 |
using TaskTrackingSystem.Shared.Models.Task;
|
| 6 |
using TaskTrackingSystem.WebApi.Features.Task;
|
|
|
|
| 7 |
|
| 8 |
namespace TaskTrackingSystem.WebApi.Features.Task
|
| 9 |
{
|
|
@@ -22,14 +23,14 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 22 |
[HttpGet]
|
| 23 |
public async Task<ActionResult<IEnumerable<TaskDto>>> GetTasks()
|
| 24 |
{
|
| 25 |
-
var tasks = await _taskService.GetAllTasksAsync();
|
| 26 |
return Ok(tasks);
|
| 27 |
}
|
| 28 |
|
| 29 |
[HttpGet("{id}")]
|
| 30 |
public async Task<ActionResult<TaskDto>> GetTask(long id)
|
| 31 |
{
|
| 32 |
-
var task = await _taskService.GetTaskByIdAsync(id);
|
| 33 |
if (task == null)
|
| 34 |
{
|
| 35 |
return NotFound(new { message = $"Task with ID {id} not found." });
|
|
@@ -40,7 +41,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 40 |
[HttpPost]
|
| 41 |
public async Task<ActionResult<Result<TaskDto>>> CreateTask([FromBody] CreateTaskDto createTaskDto)
|
| 42 |
{
|
| 43 |
-
long? currentUserId =
|
| 44 |
var result = await _taskService.CreateTaskAsync(createTaskDto, currentUserId);
|
| 45 |
return StatusCode(result.StatusCode, result);
|
| 46 |
}
|
|
@@ -48,29 +49,29 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 48 |
[HttpPut("{id}")]
|
| 49 |
public async Task<ActionResult<Result>> UpdateTask(long id, [FromBody] UpdateTaskDto updateTaskDto)
|
| 50 |
{
|
| 51 |
-
long? currentUserId =
|
| 52 |
-
var result = await _taskService.UpdateTaskAsync(id, updateTaskDto, currentUserId);
|
| 53 |
return StatusCode(result.StatusCode, result);
|
| 54 |
}
|
| 55 |
|
| 56 |
[HttpDelete("{id}")]
|
| 57 |
public async Task<ActionResult<Result>> DeleteTask(long id)
|
| 58 |
{
|
| 59 |
-
var result = await _taskService.SoftDeleteTaskAsync(id);
|
| 60 |
return StatusCode(result.StatusCode, result);
|
| 61 |
}
|
| 62 |
|
| 63 |
[HttpGet("/api/User/{userId}/tasks")]
|
| 64 |
public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetUserTasks(long userId)
|
| 65 |
{
|
| 66 |
-
var result = await _taskService.GetTasksByUserIdAsync(userId);
|
| 67 |
return StatusCode(result.StatusCode, result);
|
| 68 |
}
|
| 69 |
|
| 70 |
[HttpPatch("{id}/status")]
|
| 71 |
public async Task<IActionResult> UpdateTaskStatus(long id, [FromQuery] long statusId)
|
| 72 |
{
|
| 73 |
-
var result = await _taskService.UpdateTaskStatusAsync(id, statusId);
|
| 74 |
if (!result.IsSuccess)
|
| 75 |
{
|
| 76 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
|
|
|
| 4 |
using TaskTrackingSystem.Shared;
|
| 5 |
using TaskTrackingSystem.Shared.Models.Task;
|
| 6 |
using TaskTrackingSystem.WebApi.Features.Task;
|
| 7 |
+
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 8 |
|
| 9 |
namespace TaskTrackingSystem.WebApi.Features.Task
|
| 10 |
{
|
|
|
|
| 23 |
[HttpGet]
|
| 24 |
public async Task<ActionResult<IEnumerable<TaskDto>>> GetTasks()
|
| 25 |
{
|
| 26 |
+
var tasks = await _taskService.GetAllTasksAsync(User.GetRoleName(), User.GetUserId());
|
| 27 |
return Ok(tasks);
|
| 28 |
}
|
| 29 |
|
| 30 |
[HttpGet("{id}")]
|
| 31 |
public async Task<ActionResult<TaskDto>> GetTask(long id)
|
| 32 |
{
|
| 33 |
+
var task = await _taskService.GetTaskByIdAsync(id, User.GetRoleName(), User.GetUserId());
|
| 34 |
if (task == null)
|
| 35 |
{
|
| 36 |
return NotFound(new { message = $"Task with ID {id} not found." });
|
|
|
|
| 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);
|
| 47 |
}
|
|
|
|
| 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);
|
| 55 |
}
|
| 56 |
|
| 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 |
}
|
| 63 |
|
| 64 |
[HttpGet("/api/User/{userId}/tasks")]
|
| 65 |
public async Task<ActionResult<Result<IEnumerable<TaskDto>>>> GetUserTasks(long userId)
|
| 66 |
{
|
| 67 |
+
var result = await _taskService.GetTasksByUserIdAsync(userId, User.GetRoleName(), User.GetUserId());
|
| 68 |
return StatusCode(result.StatusCode, result);
|
| 69 |
}
|
| 70 |
|
| 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 |
{
|
| 77 |
return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
|
TaskTrackingSystem.WebApi/Features/Task/TaskService.cs
CHANGED
|
@@ -20,10 +20,9 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 20 |
_db = db;
|
| 21 |
}
|
| 22 |
|
| 23 |
-
public async Task<IEnumerable<TaskDto>> GetAllTasksAsync()
|
| 24 |
{
|
| 25 |
-
return await
|
| 26 |
-
.Where(t => t.IsDeleted != true)
|
| 27 |
.OrderBy(t => t.DueDate)
|
| 28 |
.ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
|
| 29 |
.Select(t => new TaskDto
|
|
@@ -47,11 +46,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 47 |
}).ToListAsync();
|
| 48 |
}
|
| 49 |
|
| 50 |
-
public async Task<TaskDto?> GetTaskByIdAsync(long id)
|
| 51 |
{
|
| 52 |
-
var task = await
|
| 53 |
.Include(t => t.TaskHistories)
|
| 54 |
-
.FirstOrDefaultAsync(t => t.Id == id
|
| 55 |
|
| 56 |
if (task == null) return null;
|
| 57 |
|
|
@@ -131,7 +130,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 131 |
return Result<TaskDto>.Success(resultDto, 201);
|
| 132 |
}
|
| 133 |
|
| 134 |
-
public async Task<Result> UpdateTaskAsync(long id, UpdateTaskDto dto, long? currentUserId = null)
|
| 135 |
{
|
| 136 |
if (string.IsNullOrWhiteSpace(dto.Title))
|
| 137 |
{
|
|
@@ -141,6 +140,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 141 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 142 |
if (task == null) return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
task.Title = dto.Title;
|
| 145 |
task.Description = dto.Description;
|
| 146 |
task.StatusId = dto.StatusId;
|
|
@@ -157,19 +161,29 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 157 |
return Result.Success(200);
|
| 158 |
}
|
| 159 |
|
| 160 |
-
public async Task<Result> SoftDeleteTaskAsync(long id)
|
| 161 |
{
|
| 162 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 163 |
if (task == null) return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
task.IsDeleted = true;
|
| 166 |
_db.Tasks.Update(task);
|
| 167 |
await _db.SaveChangesAsync();
|
| 168 |
return Result.Success(200);
|
| 169 |
}
|
| 170 |
|
| 171 |
-
public async Task<Result<IEnumerable<TaskDto>>> GetTasksByUserIdAsync(long userId)
|
| 172 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
var userExists = await _db.Users.AnyAsync(u => u.Id == userId && !u.IsDeleted);
|
| 174 |
if (!userExists)
|
| 175 |
{
|
|
@@ -202,7 +216,7 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 202 |
return Result<IEnumerable<TaskDto>>.Success(tasks);
|
| 203 |
}
|
| 204 |
|
| 205 |
-
public async Task<Result> UpdateTaskStatusAsync(long id, long statusId,
|
| 206 |
{
|
| 207 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 208 |
if (task == null)
|
|
@@ -210,6 +224,11 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 210 |
return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 211 |
}
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
task.StatusId = statusId;
|
| 214 |
task.UpdatedAt = DateTime.UtcNow;
|
| 215 |
task.UpdatedBy = currentUserId;
|
|
@@ -219,5 +238,35 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 219 |
|
| 220 |
return Result.Success(200);
|
| 221 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
}
|
|
|
|
| 20 |
_db = db;
|
| 21 |
}
|
| 22 |
|
| 23 |
+
public async Task<IEnumerable<TaskDto>> GetAllTasksAsync(string roleName, long currentUserId)
|
| 24 |
{
|
| 25 |
+
return await BuildAccessibleTaskQuery(roleName, currentUserId)
|
|
|
|
| 26 |
.OrderBy(t => t.DueDate)
|
| 27 |
.ThenByDescending(t => t.CreatedAt ?? DateTime.UtcNow)
|
| 28 |
.Select(t => new TaskDto
|
|
|
|
| 46 |
}).ToListAsync();
|
| 47 |
}
|
| 48 |
|
| 49 |
+
public async Task<TaskDto?> GetTaskByIdAsync(long id, string roleName, long currentUserId)
|
| 50 |
{
|
| 51 |
+
var task = await BuildAccessibleTaskQuery(roleName, currentUserId)
|
| 52 |
.Include(t => t.TaskHistories)
|
| 53 |
+
.FirstOrDefaultAsync(t => t.Id == id);
|
| 54 |
|
| 55 |
if (task == null) return null;
|
| 56 |
|
|
|
|
| 130 |
return Result<TaskDto>.Success(resultDto, 201);
|
| 131 |
}
|
| 132 |
|
| 133 |
+
public async Task<Result> UpdateTaskAsync(long id, UpdateTaskDto dto, string roleName, long? currentUserId = null)
|
| 134 |
{
|
| 135 |
if (string.IsNullOrWhiteSpace(dto.Title))
|
| 136 |
{
|
|
|
|
| 140 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 141 |
if (task == null) return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 142 |
|
| 143 |
+
if (!IsElevated(roleName) && currentUserId.HasValue && !await CanEditTaskAsync(task, currentUserId.Value))
|
| 144 |
+
{
|
| 145 |
+
return Result.Failure("You do not have access to update this task.", 403);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
task.Title = dto.Title;
|
| 149 |
task.Description = dto.Description;
|
| 150 |
task.StatusId = dto.StatusId;
|
|
|
|
| 161 |
return Result.Success(200);
|
| 162 |
}
|
| 163 |
|
| 164 |
+
public async Task<Result> SoftDeleteTaskAsync(long id, string roleName, long currentUserId)
|
| 165 |
{
|
| 166 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 167 |
if (task == null) return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 168 |
|
| 169 |
+
if (!IsElevated(roleName) && task.CreatedBy != currentUserId && task.AssignedTo != currentUserId)
|
| 170 |
+
{
|
| 171 |
+
return Result.Failure("You do not have access to delete this task.", 403);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
task.IsDeleted = true;
|
| 175 |
_db.Tasks.Update(task);
|
| 176 |
await _db.SaveChangesAsync();
|
| 177 |
return Result.Success(200);
|
| 178 |
}
|
| 179 |
|
| 180 |
+
public async Task<Result<IEnumerable<TaskDto>>> GetTasksByUserIdAsync(long userId, string roleName, long currentUserId)
|
| 181 |
{
|
| 182 |
+
if (!IsElevated(roleName) && userId != currentUserId)
|
| 183 |
+
{
|
| 184 |
+
return Result<IEnumerable<TaskDto>>.Failure("You do not have access to view these tasks.", 403);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
var userExists = await _db.Users.AnyAsync(u => u.Id == userId && !u.IsDeleted);
|
| 188 |
if (!userExists)
|
| 189 |
{
|
|
|
|
| 216 |
return Result<IEnumerable<TaskDto>>.Success(tasks);
|
| 217 |
}
|
| 218 |
|
| 219 |
+
public async Task<Result> UpdateTaskStatusAsync(long id, long statusId, string roleName, long currentUserId)
|
| 220 |
{
|
| 221 |
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == id && t.IsDeleted != true);
|
| 222 |
if (task == null)
|
|
|
|
| 224 |
return Result.Failure(ResultMessages.TaskNotFound(id), 404);
|
| 225 |
}
|
| 226 |
|
| 227 |
+
if (!IsElevated(roleName) && task.AssignedTo != currentUserId && task.CreatedBy != currentUserId)
|
| 228 |
+
{
|
| 229 |
+
return Result.Failure("You do not have access to update this task.", 403);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
task.StatusId = statusId;
|
| 233 |
task.UpdatedAt = DateTime.UtcNow;
|
| 234 |
task.UpdatedBy = currentUserId;
|
|
|
|
| 238 |
|
| 239 |
return Result.Success(200);
|
| 240 |
}
|
| 241 |
+
|
| 242 |
+
private IQueryable<TaskTrackingSystem.Database.AppDbContextModels.Task> BuildAccessibleTaskQuery(string roleName, long currentUserId)
|
| 243 |
+
{
|
| 244 |
+
var query = _db.Tasks.Where(t => t.IsDeleted != true);
|
| 245 |
+
|
| 246 |
+
if (IsElevated(roleName))
|
| 247 |
+
{
|
| 248 |
+
return query;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
return query.Where(t =>
|
| 252 |
+
t.AssignedTo == currentUserId ||
|
| 253 |
+
t.CreatedBy == currentUserId);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
private async Task<bool> CanEditTaskAsync(TaskTrackingSystem.Database.AppDbContextModels.Task task, long currentUserId)
|
| 257 |
+
{
|
| 258 |
+
if (task.CreatedBy == currentUserId || task.AssignedTo == currentUserId)
|
| 259 |
+
{
|
| 260 |
+
return true;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
return await _db.ProjectMembers.AnyAsync(pm => pm.ProjectId == task.ProjectId && pm.UserId == currentUserId);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
private static bool IsElevated(string roleName)
|
| 267 |
+
{
|
| 268 |
+
return roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase) ||
|
| 269 |
+
roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase);
|
| 270 |
+
}
|
| 271 |
}
|
| 272 |
}
|
TaskTrackingSystem.WebApi/Features/User/UserService.cs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
|
|
|
| 2 |
using System;
|
| 3 |
using System.Collections.Generic;
|
| 4 |
using System.Linq;
|
|
@@ -13,10 +14,12 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 13 |
public class UserService
|
| 14 |
{
|
| 15 |
private readonly AppDbContext _db;
|
|
|
|
| 16 |
|
| 17 |
-
public UserService(AppDbContext db)
|
| 18 |
{
|
| 19 |
_db = db;
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
|
|
@@ -33,7 +36,7 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 33 |
Phone = u.Phone,
|
| 34 |
RoleId = u.RoleId,
|
| 35 |
IsActive = u.IsActive,
|
| 36 |
-
CreatedAt =
|
| 37 |
}).ToListAsync();
|
| 38 |
}
|
| 39 |
|
|
@@ -54,7 +57,7 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 54 |
Phone = user.Phone,
|
| 55 |
RoleId = user.RoleId,
|
| 56 |
IsActive = user.IsActive,
|
| 57 |
-
CreatedAt =
|
| 58 |
};
|
| 59 |
}
|
| 60 |
|
|
@@ -77,15 +80,13 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 77 |
return Result<UserDto>.Failure("Email is already registered.", 400);
|
| 78 |
}
|
| 79 |
|
| 80 |
-
string fakePasswordHash = "HASHED_" + dto.Password;
|
| 81 |
-
|
| 82 |
var user = new TaskTrackingSystem.Database.AppDbContextModels.User
|
| 83 |
{
|
| 84 |
Username = dto.Username,
|
| 85 |
FirstName = dto.FirstName,
|
| 86 |
LastName = dto.LastName,
|
| 87 |
Email = dto.Email,
|
| 88 |
-
PasswordHash =
|
| 89 |
Phone = dto.Phone,
|
| 90 |
RoleId = dto.RoleId,
|
| 91 |
IsActive = true,
|
|
@@ -93,6 +94,8 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 93 |
CreatedBy = currentUserId
|
| 94 |
};
|
| 95 |
|
|
|
|
|
|
|
| 96 |
|
| 97 |
_db.Users.Add(user);
|
| 98 |
await _db.SaveChangesAsync();
|
|
@@ -159,4 +162,4 @@ namespace TaskTrackingSystem.WebApi.Features.User
|
|
| 159 |
return Result.Success(200);
|
| 160 |
}
|
| 161 |
}
|
| 162 |
-
}
|
|
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
| 2 |
+
using Microsoft.AspNetCore.Identity;
|
| 3 |
using System;
|
| 4 |
using System.Collections.Generic;
|
| 5 |
using System.Linq;
|
|
|
|
| 14 |
public class UserService
|
| 15 |
{
|
| 16 |
private readonly AppDbContext _db;
|
| 17 |
+
private readonly IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User> _passwordHasher;
|
| 18 |
|
| 19 |
+
public UserService(AppDbContext db, IPasswordHasher<TaskTrackingSystem.Database.AppDbContextModels.User> passwordHasher)
|
| 20 |
{
|
| 21 |
_db = db;
|
| 22 |
+
_passwordHasher = passwordHasher;
|
| 23 |
}
|
| 24 |
|
| 25 |
public async Task<IEnumerable<UserDto>> GetAllUsersAsync()
|
|
|
|
| 36 |
Phone = u.Phone,
|
| 37 |
RoleId = u.RoleId,
|
| 38 |
IsActive = u.IsActive,
|
| 39 |
+
CreatedAt = u.CreatedAt ?? DateTime.UtcNow
|
| 40 |
}).ToListAsync();
|
| 41 |
}
|
| 42 |
|
|
|
|
| 57 |
Phone = user.Phone,
|
| 58 |
RoleId = user.RoleId,
|
| 59 |
IsActive = user.IsActive,
|
| 60 |
+
CreatedAt = user.CreatedAt ?? DateTime.UtcNow
|
| 61 |
};
|
| 62 |
}
|
| 63 |
|
|
|
|
| 80 |
return Result<UserDto>.Failure("Email is already registered.", 400);
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
| 83 |
var user = new TaskTrackingSystem.Database.AppDbContextModels.User
|
| 84 |
{
|
| 85 |
Username = dto.Username,
|
| 86 |
FirstName = dto.FirstName,
|
| 87 |
LastName = dto.LastName,
|
| 88 |
Email = dto.Email,
|
| 89 |
+
PasswordHash = string.Empty,
|
| 90 |
Phone = dto.Phone,
|
| 91 |
RoleId = dto.RoleId,
|
| 92 |
IsActive = true,
|
|
|
|
| 94 |
CreatedBy = currentUserId
|
| 95 |
};
|
| 96 |
|
| 97 |
+
user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password);
|
| 98 |
+
|
| 99 |
|
| 100 |
_db.Users.Add(user);
|
| 101 |
await _db.SaveChangesAsync();
|
|
|
|
| 162 |
return Result.Success(200);
|
| 163 |
}
|
| 164 |
}
|
| 165 |
+
}
|
TaskTrackingSystem.WebApi/Infrastructure/ClaimsPrincipalExtensions.cs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System.Security.Claims;
|
| 2 |
+
|
| 3 |
+
namespace TaskTrackingSystem.WebApi.Infrastructure;
|
| 4 |
+
|
| 5 |
+
public static class ClaimsPrincipalExtensions
|
| 6 |
+
{
|
| 7 |
+
public static long GetUserId(this ClaimsPrincipal user)
|
| 8 |
+
{
|
| 9 |
+
var value = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
| 10 |
+
return long.TryParse(value, out var id) ? id : 0;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
public static long GetRoleId(this ClaimsPrincipal user)
|
| 14 |
+
{
|
| 15 |
+
var value = user.FindFirstValue("role_id");
|
| 16 |
+
return long.TryParse(value, out var id) ? id : 0;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
public static string GetRoleName(this ClaimsPrincipal user)
|
| 20 |
+
{
|
| 21 |
+
return user.FindFirstValue(ClaimTypes.Role) ?? string.Empty;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
public static bool IsAdmin(this ClaimsPrincipal user)
|
| 25 |
+
{
|
| 26 |
+
return user.IsInRole("Admin");
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
public static bool IsManager(this ClaimsPrincipal user)
|
| 30 |
+
{
|
| 31 |
+
return user.IsInRole("Manager");
|
| 32 |
+
}
|
| 33 |
+
}
|
TaskTrackingSystem.WebApi/Program.cs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
| 2 |
using Microsoft.AspNetCore.Authorization;
|
|
|
|
| 3 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
| 4 |
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
| 5 |
using System.Text;
|
|
@@ -29,6 +30,7 @@ builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Task.TaskService>(
|
|
| 29 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Dashboard.DashboardService>();
|
| 30 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Report.ReportService>();
|
| 31 |
builder.Services.AddScoped<TaskTrackingSystem.WebApi.Features.Menu.MenuService>();
|
|
|
|
| 32 |
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
| 33 |
builder.Services.AddAuthorization();
|
| 34 |
builder.Services.AddEndpointsApiExplorer();
|
|
@@ -40,7 +42,7 @@ builder.Services.AddAuthentication(options =>
|
|
| 40 |
}).AddJwtBearer(options =>
|
| 41 |
{
|
| 42 |
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
| 43 |
-
var key = jwtSettings["Key"];
|
| 44 |
options.TokenValidationParameters = new TokenValidationParameters
|
| 45 |
{
|
| 46 |
ValidateIssuer = true,
|
|
|
|
| 1 |
using Microsoft.EntityFrameworkCore;
|
| 2 |
using Microsoft.AspNetCore.Authorization;
|
| 3 |
+
using Microsoft.AspNetCore.Identity;
|
| 4 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
| 6 |
using System.Text;
|
|
|
|
| 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();
|
| 36 |
builder.Services.AddEndpointsApiExplorer();
|
|
|
|
| 42 |
}).AddJwtBearer(options =>
|
| 43 |
{
|
| 44 |
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
| 45 |
+
var key = jwtSettings["Key"] ?? "SUPER_SECRET_KEY_THAT_IS_LONG_ENOUGH_12345";
|
| 46 |
options.TokenValidationParameters = new TokenValidationParameters
|
| 47 |
{
|
| 48 |
ValidateIssuer = true,
|
TaskTrackingSystem.WebApi/appsettings.json
CHANGED
|
@@ -11,7 +11,10 @@
|
|
| 11 |
"Issuer": "TaskTrackingSystem",
|
| 12 |
"Audience": "Users"
|
| 13 |
},
|
|
|
|
|
|
|
|
|
|
| 14 |
"ConnectionStrings": {
|
| 15 |
"DefaultConnection": "Server=.;Database=TTS;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=5;"
|
| 16 |
}
|
| 17 |
-
}
|
|
|
|
| 11 |
"Issuer": "TaskTrackingSystem",
|
| 12 |
"Audience": "Users"
|
| 13 |
},
|
| 14 |
+
"PasswordReset": {
|
| 15 |
+
"RecoveryCode": "TASKIFY-RESET-2026"
|
| 16 |
+
},
|
| 17 |
"ConnectionStrings": {
|
| 18 |
"DefaultConnection": "Server=.;Database=TTS;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=5;"
|
| 19 |
}
|
| 20 |
+
}
|
TaskTrackingSystem.WebApp/AccountEndpoints.cs
CHANGED
|
@@ -15,8 +15,9 @@ public static class AccountEndpoints
|
|
| 15 |
{
|
| 16 |
app.MapPost("/account/login", LoginAsync);
|
| 17 |
app.MapPost("/account/register", RegisterAsync);
|
| 18 |
-
app.
|
| 19 |
-
app.
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
private static async Task<IResult> LoginAsync(
|
|
@@ -155,10 +156,57 @@ public static class AccountEndpoints
|
|
| 155 |
return Results.Redirect($"/register?error={Uri.EscapeDataString(errorMessage)}");
|
| 156 |
}
|
| 157 |
|
| 158 |
-
private static async Task<IResult>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
{
|
| 160 |
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
| 161 |
context.Response.Cookies.Delete(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
|
|
| 162 |
return Results.Redirect("/login?loggedOut=true");
|
| 163 |
}
|
| 164 |
|
|
@@ -175,6 +223,11 @@ public static class AccountEndpoints
|
|
| 175 |
claims.Add(new Claim(ClaimTypes.Role, authResult.RoleName));
|
| 176 |
}
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
var userId = TryGetUserIdFromJwt(authResult.Token);
|
| 179 |
if (!string.IsNullOrWhiteSpace(userId))
|
| 180 |
{
|
|
|
|
| 15 |
{
|
| 16 |
app.MapPost("/account/login", LoginAsync);
|
| 17 |
app.MapPost("/account/register", RegisterAsync);
|
| 18 |
+
app.MapPost("/account/reset-password", ResetPasswordAsync);
|
| 19 |
+
app.MapGet("/account/logout", LogoutAsync);
|
| 20 |
+
app.MapPost("/account/logout", LogoutAsync);
|
| 21 |
}
|
| 22 |
|
| 23 |
private static async Task<IResult> LoginAsync(
|
|
|
|
| 156 |
return Results.Redirect($"/register?error={Uri.EscapeDataString(errorMessage)}");
|
| 157 |
}
|
| 158 |
|
| 159 |
+
private static async Task<IResult> ResetPasswordAsync(
|
| 160 |
+
HttpContext context,
|
| 161 |
+
IHttpClientFactory httpClientFactory,
|
| 162 |
+
[FromForm] ResetPasswordDto resetPasswordDto)
|
| 163 |
+
{
|
| 164 |
+
HttpResponseMessage response;
|
| 165 |
+
try
|
| 166 |
+
{
|
| 167 |
+
var client = httpClientFactory.CreateClient("WebApi");
|
| 168 |
+
response = await client.PostAsJsonAsync("Auth/reset-password", resetPasswordDto);
|
| 169 |
+
}
|
| 170 |
+
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or TimeoutException)
|
| 171 |
+
{
|
| 172 |
+
return Results.Redirect($"/reset-password?error={Uri.EscapeDataString("Unable to reach the API server. Please ensure TaskTrackingSystem.WebApi is running.")}");
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
if (response.IsSuccessStatusCode)
|
| 176 |
+
{
|
| 177 |
+
var result = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
|
| 178 |
+
if (result?.IsSuccess == true)
|
| 179 |
+
{
|
| 180 |
+
return Results.Redirect("/login?reset=true");
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
var errorMessage = "Failed to reset password.";
|
| 185 |
+
if (response.Content != null)
|
| 186 |
+
{
|
| 187 |
+
try
|
| 188 |
+
{
|
| 189 |
+
var contentString = await response.Content.ReadAsStringAsync();
|
| 190 |
+
var result = JsonSerializer.Deserialize<Result>(contentString, Serialization.CaseInsensitive);
|
| 191 |
+
if (result != null && !string.IsNullOrWhiteSpace(result.ErrorMessage))
|
| 192 |
+
{
|
| 193 |
+
errorMessage = result.ErrorMessage;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
catch
|
| 197 |
+
{
|
| 198 |
+
// Keep default error message.
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
return Results.Redirect($"/reset-password?error={Uri.EscapeDataString(errorMessage)}");
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
private static async Task<IResult> LogoutAsync(HttpContext context, UserSessionState sessionState)
|
| 206 |
{
|
| 207 |
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
| 208 |
context.Response.Cookies.Delete(CookieAuthenticationDefaults.AuthenticationScheme);
|
| 209 |
+
sessionState.ClearSession();
|
| 210 |
return Results.Redirect("/login?loggedOut=true");
|
| 211 |
}
|
| 212 |
|
|
|
|
| 223 |
claims.Add(new Claim(ClaimTypes.Role, authResult.RoleName));
|
| 224 |
}
|
| 225 |
|
| 226 |
+
if (authResult.RoleId > 0)
|
| 227 |
+
{
|
| 228 |
+
claims.Add(new Claim("role_id", authResult.RoleId.ToString()));
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
var userId = TryGetUserIdFromJwt(authResult.Token);
|
| 232 |
if (!string.IsNullOrWhiteSpace(userId))
|
| 233 |
{
|
TaskTrackingSystem.WebApp/Components/CustomAuthenticationStateProvider.cs
CHANGED
|
@@ -48,5 +48,18 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
|
| 48 |
{
|
| 49 |
_sessionState.Token = token;
|
| 50 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
}
|
|
|
|
| 48 |
{
|
| 49 |
_sessionState.Token = token;
|
| 50 |
}
|
| 51 |
+
|
| 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 |
}
|
TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor
CHANGED
|
@@ -135,8 +135,8 @@
|
|
| 135 |
return;
|
| 136 |
}
|
| 137 |
|
| 138 |
-
await MenuAuthorization.GetUserMenusAsync(user);
|
| 139 |
-
isAuthorizedPage = MenuAuthorization.IsRouteAllowed(
|
| 140 |
await InvokeAsync(StateHasChanged);
|
| 141 |
}
|
| 142 |
|
|
|
|
| 135 |
return;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
var menus = await MenuAuthorization.GetUserMenusAsync(user);
|
| 139 |
+
isAuthorizedPage = MenuAuthorization.IsRouteAllowed(menus, relativePath);
|
| 140 |
await InvokeAsync(StateHasChanged);
|
| 141 |
}
|
| 142 |
|
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
|
|
| 1 |
@using Microsoft.AspNetCore.Components.Authorization
|
|
|
|
| 2 |
@using TaskTrackingSystem.Shared.Models.Menu
|
| 3 |
@using Microsoft.JSInterop
|
| 4 |
@implements IDisposable
|
|
@@ -7,25 +9,16 @@
|
|
| 7 |
@inject NavigationManager Navigation
|
| 8 |
@inject IJSRuntime JS
|
| 9 |
|
| 10 |
-
|
| 11 |
<nav class="flex-1 space-y-1 px-4 py-6 overflow-y-auto">
|
| 12 |
@if (menus.Any())
|
| 13 |
{
|
| 14 |
@foreach (var menu in menus)
|
| 15 |
{
|
| 16 |
-
|
| 17 |
{
|
| 18 |
-
<!-- Parent with sub-menus: collapsible dropdown -->
|
| 19 |
<button class="nav-dropdown-toggle @(IsMenuGroupExpanded(menu.MenuCode) ? "expanded" : "")"
|
| 20 |
@onclick="() => ToggleMenuGroup(menu.MenuCode)">
|
| 21 |
-
@
|
| 22 |
-
{
|
| 23 |
-
<i data-lucide="@menu.Icon" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 24 |
-
}
|
| 25 |
-
else
|
| 26 |
-
{
|
| 27 |
-
<i data-lucide="circle" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 28 |
-
}
|
| 29 |
<span class="flex-1 text-left">@menu.MenuName</span>
|
| 30 |
<i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
|
| 31 |
</button>
|
|
@@ -37,14 +30,7 @@
|
|
| 37 |
<NavLink class="nav-link-item nav-child-item"
|
| 38 |
href="@MenuAuthorizationService.NormalizeMenuHref(sub.MenuUrl)"
|
| 39 |
Match="@GetNavLinkMatch(sub.MenuUrl)">
|
| 40 |
-
@
|
| 41 |
-
{
|
| 42 |
-
<i data-lucide="@sub.Icon" class="h-3.5 w-3.5 mr-3 text-slate-400 shrink-0"></i>
|
| 43 |
-
}
|
| 44 |
-
else
|
| 45 |
-
{
|
| 46 |
-
<i data-lucide="circle" class="h-3.5 w-3.5 mr-3 text-slate-400 shrink-0"></i>
|
| 47 |
-
}
|
| 48 |
<span>@sub.MenuName</span>
|
| 49 |
</NavLink>
|
| 50 |
}
|
|
@@ -53,70 +39,27 @@
|
|
| 53 |
}
|
| 54 |
else
|
| 55 |
{
|
| 56 |
-
<!-- Simple nav link -->
|
| 57 |
<NavLink class="nav-link-item"
|
| 58 |
href="@MenuAuthorizationService.NormalizeMenuHref(menu.MenuUrl)"
|
| 59 |
Match="@(string.IsNullOrWhiteSpace(menu.MenuUrl) || menu.MenuUrl.Trim('/') is "dashboard" or "" ? NavLinkMatch.All : NavLinkMatch.Prefix)">
|
| 60 |
-
@
|
| 61 |
-
{
|
| 62 |
-
<i data-lucide="@menu.Icon" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 63 |
-
}
|
| 64 |
-
else
|
| 65 |
-
{
|
| 66 |
-
<i data-lucide="circle" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 67 |
-
}
|
| 68 |
<span>@menu.MenuName</span>
|
| 69 |
</NavLink>
|
| 70 |
}
|
| 71 |
}
|
| 72 |
}
|
| 73 |
-
else
|
| 74 |
{
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
{
|
| 79 |
-
<!-- Fallback parent with children: collapsible dropdown -->
|
| 80 |
-
<button class="nav-dropdown-toggle @(IsFallbackGroupExpanded(link.Label) ? "expanded" : "")"
|
| 81 |
-
@onclick="() => ToggleFallbackGroup(link.Label)">
|
| 82 |
-
<i data-lucide="@link.Icon" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 83 |
-
<span class="flex-1 text-left">@link.Label</span>
|
| 84 |
-
<i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
|
| 85 |
-
</button>
|
| 86 |
-
@if (IsFallbackGroupExpanded(link.Label))
|
| 87 |
-
{
|
| 88 |
-
<div class="nav-dropdown-children">
|
| 89 |
-
@foreach (var child in link.Children)
|
| 90 |
-
{
|
| 91 |
-
<NavLink class="nav-link-item nav-child-item"
|
| 92 |
-
href="@child.Href" Match="@child.Match">
|
| 93 |
-
<i data-lucide="@child.Icon" class="h-3.5 w-3.5 mr-3 text-slate-400 shrink-0"></i>
|
| 94 |
-
<span>@child.Label</span>
|
| 95 |
-
</NavLink>
|
| 96 |
-
}
|
| 97 |
-
</div>
|
| 98 |
-
}
|
| 99 |
-
}
|
| 100 |
-
else
|
| 101 |
-
{
|
| 102 |
-
<!-- Simple fallback link -->
|
| 103 |
-
<NavLink class="nav-link-item"
|
| 104 |
-
href="@link.Href" Match="@link.Match">
|
| 105 |
-
<i data-lucide="@link.Icon" class="h-4 w-4 mr-3 text-slate-400 shrink-0"></i>
|
| 106 |
-
<span>@link.Label</span>
|
| 107 |
-
</NavLink>
|
| 108 |
-
}
|
| 109 |
-
}
|
| 110 |
}
|
| 111 |
</nav>
|
| 112 |
|
| 113 |
@code {
|
| 114 |
private List<MenuDto> menus = new();
|
| 115 |
-
private bool showRoleFallback;
|
| 116 |
-
private List<FallbackLink> fallbackLinks = new();
|
| 117 |
private string currentPath = string.Empty;
|
| 118 |
private HashSet<string> expandedMenuGroups = new();
|
| 119 |
-
private HashSet<string> expandedFallbackGroups = new();
|
| 120 |
|
| 121 |
protected override async Task OnInitializedAsync()
|
| 122 |
{
|
|
@@ -130,33 +73,14 @@
|
|
| 130 |
|
| 131 |
if (user.Identity?.IsAuthenticated == true)
|
| 132 |
{
|
| 133 |
-
fallbackLinks = BuildFallbackLinks(user);
|
| 134 |
-
showRoleFallback = fallbackLinks.Count > 0;
|
| 135 |
-
|
| 136 |
-
// Auto-expand fallback groups whose children match the current route
|
| 137 |
-
foreach (var link in fallbackLinks)
|
| 138 |
-
{
|
| 139 |
-
if (link.Children != null && IsFallbackExpanded(link.Children))
|
| 140 |
-
{
|
| 141 |
-
expandedFallbackGroups.Add(link.Label);
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
menus = await MenuAuthorization.GetUserMenusAsync(user);
|
| 146 |
-
|
| 147 |
-
{
|
| 148 |
-
showRoleFallback = false;
|
| 149 |
-
|
| 150 |
-
// Build URL-based hierarchy for menus that have no SubMenus from the API
|
| 151 |
-
BuildUrlHierarchy(menus);
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 155 |
{
|
| 156 |
-
|
| 157 |
-
{
|
| 158 |
-
expandedMenuGroups.Add(menu.MenuCode);
|
| 159 |
-
}
|
| 160 |
}
|
| 161 |
}
|
| 162 |
}
|
|
@@ -171,14 +95,6 @@
|
|
| 171 |
{
|
| 172 |
currentPath = Navigation.ToBaseRelativePath(e.Location).Trim('/');
|
| 173 |
|
| 174 |
-
// Auto-expand groups on navigation
|
| 175 |
-
foreach (var link in fallbackLinks)
|
| 176 |
-
{
|
| 177 |
-
if (link.Children != null && IsFallbackExpanded(link.Children))
|
| 178 |
-
{
|
| 179 |
-
expandedFallbackGroups.Add(link.Label);
|
| 180 |
-
}
|
| 181 |
-
}
|
| 182 |
foreach (var menu in menus)
|
| 183 |
{
|
| 184 |
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
|
|
@@ -190,21 +106,15 @@
|
|
| 190 |
InvokeAsync(StateHasChanged);
|
| 191 |
}
|
| 192 |
|
| 193 |
-
// Toggle helpers
|
| 194 |
private void ToggleMenuGroup(string menuCode)
|
| 195 |
{
|
| 196 |
if (!expandedMenuGroups.Remove(menuCode))
|
|
|
|
| 197 |
expandedMenuGroups.Add(menuCode);
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
private void ToggleFallbackGroup(string label)
|
| 201 |
-
{
|
| 202 |
-
if (!expandedFallbackGroups.Remove(label))
|
| 203 |
-
expandedFallbackGroups.Add(label);
|
| 204 |
}
|
| 205 |
|
| 206 |
private bool IsMenuGroupExpanded(string menuCode) => expandedMenuGroups.Contains(menuCode);
|
| 207 |
-
private bool IsFallbackGroupExpanded(string label) => expandedFallbackGroups.Contains(label);
|
| 208 |
|
| 209 |
private bool IsMenuExpanded(string menuCode, IEnumerable<MenuDto> subMenus)
|
| 210 |
{
|
|
@@ -213,12 +123,6 @@
|
|
| 213 |
currentPath.StartsWith(s.MenuUrl.Trim('/'), StringComparison.OrdinalIgnoreCase));
|
| 214 |
}
|
| 215 |
|
| 216 |
-
private bool IsFallbackExpanded(IEnumerable<FallbackLink> children)
|
| 217 |
-
{
|
| 218 |
-
return children.Any(c =>
|
| 219 |
-
currentPath.StartsWith(c.Href.TrimStart('/'), StringComparison.OrdinalIgnoreCase));
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
private static NavLinkMatch GetNavLinkMatch(string? menuUrl)
|
| 223 |
{
|
| 224 |
return string.IsNullOrWhiteSpace(menuUrl) || menuUrl.Trim('/') is "dashboard" or ""
|
|
@@ -226,12 +130,6 @@
|
|
| 226 |
: NavLinkMatch.Prefix;
|
| 227 |
}
|
| 228 |
|
| 229 |
-
/// <summary>
|
| 230 |
-
/// Post-processes a flat menu list: if a parent menu (e.g. /reports) has no SubMenus
|
| 231 |
-
/// but other top-level items share that URL prefix (e.g. /reports/employees),
|
| 232 |
-
/// nest them as SubMenus and remove from the top-level list.
|
| 233 |
-
/// Also injects known report sub-pages if the Reports menu has no children.
|
| 234 |
-
/// </summary>
|
| 235 |
private static void BuildUrlHierarchy(List<MenuDto> menuList)
|
| 236 |
{
|
| 237 |
var toRemove = new List<MenuDto>();
|
|
@@ -239,17 +137,21 @@
|
|
| 239 |
foreach (var parent in menuList)
|
| 240 |
{
|
| 241 |
if (string.IsNullOrWhiteSpace(parent.MenuUrl))
|
|
|
|
| 242 |
continue;
|
|
|
|
| 243 |
|
| 244 |
-
// Only process parents that don't already have SubMenus from the API
|
| 245 |
if (parent.SubMenus != null && parent.SubMenus.Count > 0)
|
|
|
|
| 246 |
continue;
|
|
|
|
| 247 |
|
| 248 |
var parentPath = parent.MenuUrl.Trim('/');
|
| 249 |
if (string.IsNullOrEmpty(parentPath))
|
|
|
|
| 250 |
continue;
|
|
|
|
| 251 |
|
| 252 |
-
// Find other top-level items whose URL starts with this parent's URL + "/"
|
| 253 |
var children = menuList
|
| 254 |
.Where(m => m != parent
|
| 255 |
&& !string.IsNullOrWhiteSpace(m.MenuUrl)
|
|
@@ -268,99 +170,23 @@
|
|
| 268 |
{
|
| 269 |
menuList.Remove(item);
|
| 270 |
}
|
| 271 |
-
|
| 272 |
-
// Inject known report sub-pages if the Reports menu exists but still has no children
|
| 273 |
-
var reportsMenu = menuList.FirstOrDefault(m =>
|
| 274 |
-
!string.IsNullOrWhiteSpace(m.MenuUrl) &&
|
| 275 |
-
m.MenuUrl.Trim('/').Equals("reports", StringComparison.OrdinalIgnoreCase));
|
| 276 |
-
|
| 277 |
-
if (reportsMenu != null && (reportsMenu.SubMenus == null || reportsMenu.SubMenus.Count == 0))
|
| 278 |
-
{
|
| 279 |
-
reportsMenu.SubMenus = new List<MenuDto>
|
| 280 |
-
{
|
| 281 |
-
new() { MenuCode = "RPT_EMP", MenuName = "Employee Report", MenuUrl = "/reports/employees", Icon = "users", OrderNo = 1 },
|
| 282 |
-
new() { MenuCode = "RPT_PRJ", MenuName = "Project Progress", MenuUrl = "/reports/projects", Icon = "folder-kanban", OrderNo = 2 },
|
| 283 |
-
new() { MenuCode = "RPT_TSK", MenuName = "Task Report", MenuUrl = "/reports/tasks", Icon = "check-square", OrderNo = 3 },
|
| 284 |
-
new() { MenuCode = "RPT_OVD", MenuName = "Overdue Tasks", MenuUrl = "/reports/overdue", Icon = "alert-circle", OrderNo = 4 },
|
| 285 |
-
new() { MenuCode = "RPT_TMS", MenuName = "Time Tracking", MenuUrl = "/reports/timesheet", Icon = "clock", OrderNo = 5 }
|
| 286 |
-
};
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
// Inject known project sub-pages if the Projects menu exists but still has no children
|
| 290 |
-
var projectsMenu = menuList.FirstOrDefault(m =>
|
| 291 |
-
!string.IsNullOrWhiteSpace(m.MenuUrl) &&
|
| 292 |
-
m.MenuUrl.Trim('/').Equals("projects", StringComparison.OrdinalIgnoreCase));
|
| 293 |
-
|
| 294 |
-
if (projectsMenu != null && (projectsMenu.SubMenus == null || projectsMenu.SubMenus.Count == 0))
|
| 295 |
-
{
|
| 296 |
-
projectsMenu.SubMenus = new List<MenuDto>
|
| 297 |
-
{
|
| 298 |
-
new() { MenuCode = "PRJ_ALL", MenuName = "All Projects", MenuUrl = "/projects/all", Icon = "folder-kanban", OrderNo = 1 },
|
| 299 |
-
new() { MenuCode = "PRJ_ASN", MenuName = "Project Assign", MenuUrl = "/projects/assign", Icon = "user-plus", OrderNo = 2 }
|
| 300 |
-
};
|
| 301 |
-
}
|
| 302 |
-
|
| 303 |
-
// Inject known task sub-pages if the Tasks menu exists but still has no children
|
| 304 |
-
var tasksMenu = menuList.FirstOrDefault(m =>
|
| 305 |
-
!string.IsNullOrWhiteSpace(m.MenuUrl) &&
|
| 306 |
-
m.MenuUrl.Trim('/').Equals("tasks", StringComparison.OrdinalIgnoreCase));
|
| 307 |
-
|
| 308 |
-
if (tasksMenu != null && (tasksMenu.SubMenus == null || tasksMenu.SubMenus.Count == 0))
|
| 309 |
-
{
|
| 310 |
-
tasksMenu.SubMenus = new List<MenuDto>
|
| 311 |
-
{
|
| 312 |
-
new() { MenuCode = "TSK_ALL", MenuName = "All Tasks", MenuUrl = "/tasks/all", Icon = "check-square", OrderNo = 1 },
|
| 313 |
-
new() { MenuCode = "TSK_ASN", MenuName = "Task Assign", MenuUrl = "/tasks/assign", Icon = "user-check", OrderNo = 2 }
|
| 314 |
-
};
|
| 315 |
-
}
|
| 316 |
}
|
| 317 |
|
| 318 |
-
private static
|
| 319 |
{
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
var taskChildren = new List<FallbackLink>
|
| 326 |
-
{
|
| 327 |
-
new("/tasks/all", "All Tasks", "check-square", NavLinkMatch.Prefix)
|
| 328 |
-
};
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
{
|
| 338 |
-
new("/dashboard", "Dashboard", "layout-dashboard", NavLinkMatch.All),
|
| 339 |
-
new FallbackLink("/projects", "Projects", "folder-kanban", NavLinkMatch.Prefix, projectChildren),
|
| 340 |
-
new FallbackLink("/tasks", "Tasks", "check-square", NavLinkMatch.Prefix, taskChildren)
|
| 341 |
-
};
|
| 342 |
-
|
| 343 |
-
if (role.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 344 |
-
{
|
| 345 |
-
links.Add(new FallbackLink(
|
| 346 |
-
"/reports",
|
| 347 |
-
"Reports",
|
| 348 |
-
"bar-chart-3",
|
| 349 |
-
NavLinkMatch.Prefix,
|
| 350 |
-
new List<FallbackLink>
|
| 351 |
-
{
|
| 352 |
-
new("/reports/employees", "Employee Report", "users", NavLinkMatch.Prefix),
|
| 353 |
-
new("/reports/projects", "Project Progress", "folder-kanban", NavLinkMatch.Prefix),
|
| 354 |
-
new("/reports/tasks", "Task Report", "check-square", NavLinkMatch.Prefix),
|
| 355 |
-
new("/reports/overdue", "Overdue Tasks", "alert-circle", NavLinkMatch.Prefix),
|
| 356 |
-
new("/reports/timesheet", "Time Tracking", "clock", NavLinkMatch.Prefix)
|
| 357 |
-
}));
|
| 358 |
-
links.Add(new("/roles", "Roles", "shield-check", NavLinkMatch.Prefix));
|
| 359 |
-
links.Add(new("/users", "Users", "users", NavLinkMatch.Prefix));
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
return links;
|
| 363 |
-
}
|
| 364 |
|
| 365 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 366 |
{
|
|
@@ -370,7 +196,6 @@
|
|
| 370 |
}
|
| 371 |
catch
|
| 372 |
{
|
| 373 |
-
// Ignore JS errors during prerendering or if JS is not available yet
|
| 374 |
}
|
| 375 |
}
|
| 376 |
|
|
@@ -378,11 +203,4 @@
|
|
| 378 |
{
|
| 379 |
Navigation.LocationChanged -= OnLocationChanged;
|
| 380 |
}
|
| 381 |
-
|
| 382 |
-
private sealed record FallbackLink(
|
| 383 |
-
string Href,
|
| 384 |
-
string Label,
|
| 385 |
-
string Icon,
|
| 386 |
-
NavLinkMatch Match,
|
| 387 |
-
List<FallbackLink>? Children = null);
|
| 388 |
}
|
|
|
|
| 1 |
+
@using Microsoft.AspNetCore.Components
|
| 2 |
@using Microsoft.AspNetCore.Components.Authorization
|
| 3 |
+
@using Microsoft.AspNetCore.Components.Routing
|
| 4 |
@using TaskTrackingSystem.Shared.Models.Menu
|
| 5 |
@using Microsoft.JSInterop
|
| 6 |
@implements IDisposable
|
|
|
|
| 9 |
@inject NavigationManager Navigation
|
| 10 |
@inject IJSRuntime JS
|
| 11 |
|
|
|
|
| 12 |
<nav class="flex-1 space-y-1 px-4 py-6 overflow-y-auto">
|
| 13 |
@if (menus.Any())
|
| 14 |
{
|
| 15 |
@foreach (var menu in menus)
|
| 16 |
{
|
| 17 |
+
if (menu.SubMenus != null && menu.SubMenus.Count > 0)
|
| 18 |
{
|
|
|
|
| 19 |
<button class="nav-dropdown-toggle @(IsMenuGroupExpanded(menu.MenuCode) ? "expanded" : "")"
|
| 20 |
@onclick="() => ToggleMenuGroup(menu.MenuCode)">
|
| 21 |
+
@RenderIcon(menu.Icon)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
<span class="flex-1 text-left">@menu.MenuName</span>
|
| 23 |
<i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
|
| 24 |
</button>
|
|
|
|
| 30 |
<NavLink class="nav-link-item nav-child-item"
|
| 31 |
href="@MenuAuthorizationService.NormalizeMenuHref(sub.MenuUrl)"
|
| 32 |
Match="@GetNavLinkMatch(sub.MenuUrl)">
|
| 33 |
+
@RenderSmallIcon(sub.Icon)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
<span>@sub.MenuName</span>
|
| 35 |
</NavLink>
|
| 36 |
}
|
|
|
|
| 39 |
}
|
| 40 |
else
|
| 41 |
{
|
|
|
|
| 42 |
<NavLink class="nav-link-item"
|
| 43 |
href="@MenuAuthorizationService.NormalizeMenuHref(menu.MenuUrl)"
|
| 44 |
Match="@(string.IsNullOrWhiteSpace(menu.MenuUrl) || menu.MenuUrl.Trim('/') is "dashboard" or "" ? NavLinkMatch.All : NavLinkMatch.Prefix)">
|
| 45 |
+
@RenderIcon(menu.Icon)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
<span>@menu.MenuName</span>
|
| 47 |
</NavLink>
|
| 48 |
}
|
| 49 |
}
|
| 50 |
}
|
| 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>
|
| 58 |
|
| 59 |
@code {
|
| 60 |
private List<MenuDto> menus = new();
|
|
|
|
|
|
|
| 61 |
private string currentPath = string.Empty;
|
| 62 |
private HashSet<string> expandedMenuGroups = new();
|
|
|
|
| 63 |
|
| 64 |
protected override async Task OnInitializedAsync()
|
| 65 |
{
|
|
|
|
| 73 |
|
| 74 |
if (user.Identity?.IsAuthenticated == true)
|
| 75 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
menus = await MenuAuthorization.GetUserMenusAsync(user);
|
| 77 |
+
BuildUrlHierarchy(menus);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
foreach (var menu in menus)
|
| 80 |
+
{
|
| 81 |
+
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
|
| 82 |
{
|
| 83 |
+
expandedMenuGroups.Add(menu.MenuCode);
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
}
|
| 86 |
}
|
|
|
|
| 95 |
{
|
| 96 |
currentPath = Navigation.ToBaseRelativePath(e.Location).Trim('/');
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
foreach (var menu in menus)
|
| 99 |
{
|
| 100 |
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
|
|
|
|
| 106 |
InvokeAsync(StateHasChanged);
|
| 107 |
}
|
| 108 |
|
|
|
|
| 109 |
private void ToggleMenuGroup(string menuCode)
|
| 110 |
{
|
| 111 |
if (!expandedMenuGroups.Remove(menuCode))
|
| 112 |
+
{
|
| 113 |
expandedMenuGroups.Add(menuCode);
|
| 114 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
private bool IsMenuGroupExpanded(string menuCode) => expandedMenuGroups.Contains(menuCode);
|
|
|
|
| 118 |
|
| 119 |
private bool IsMenuExpanded(string menuCode, IEnumerable<MenuDto> subMenus)
|
| 120 |
{
|
|
|
|
| 123 |
currentPath.StartsWith(s.MenuUrl.Trim('/'), StringComparison.OrdinalIgnoreCase));
|
| 124 |
}
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
private static NavLinkMatch GetNavLinkMatch(string? menuUrl)
|
| 127 |
{
|
| 128 |
return string.IsNullOrWhiteSpace(menuUrl) || menuUrl.Trim('/') is "dashboard" or ""
|
|
|
|
| 130 |
: NavLinkMatch.Prefix;
|
| 131 |
}
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
private static void BuildUrlHierarchy(List<MenuDto> menuList)
|
| 134 |
{
|
| 135 |
var toRemove = new List<MenuDto>();
|
|
|
|
| 137 |
foreach (var parent in menuList)
|
| 138 |
{
|
| 139 |
if (string.IsNullOrWhiteSpace(parent.MenuUrl))
|
| 140 |
+
{
|
| 141 |
continue;
|
| 142 |
+
}
|
| 143 |
|
|
|
|
| 144 |
if (parent.SubMenus != null && parent.SubMenus.Count > 0)
|
| 145 |
+
{
|
| 146 |
continue;
|
| 147 |
+
}
|
| 148 |
|
| 149 |
var parentPath = parent.MenuUrl.Trim('/');
|
| 150 |
if (string.IsNullOrEmpty(parentPath))
|
| 151 |
+
{
|
| 152 |
continue;
|
| 153 |
+
}
|
| 154 |
|
|
|
|
| 155 |
var children = menuList
|
| 156 |
.Where(m => m != parent
|
| 157 |
&& !string.IsNullOrWhiteSpace(m.MenuUrl)
|
|
|
|
| 170 |
{
|
| 171 |
menuList.Remove(item);
|
| 172 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
|
| 175 |
+
private static RenderFragment RenderIcon(string? icon) => builder =>
|
| 176 |
{
|
| 177 |
+
builder.OpenElement(0, "i");
|
| 178 |
+
builder.AddAttribute(1, "data-lucide", string.IsNullOrWhiteSpace(icon) ? "circle" : icon);
|
| 179 |
+
builder.AddAttribute(2, "class", "h-4 w-4 mr-3 text-slate-400 shrink-0");
|
| 180 |
+
builder.CloseElement();
|
| 181 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
+
private static RenderFragment RenderSmallIcon(string? icon) => builder =>
|
| 184 |
+
{
|
| 185 |
+
builder.OpenElement(0, "i");
|
| 186 |
+
builder.AddAttribute(1, "data-lucide", string.IsNullOrWhiteSpace(icon) ? "circle" : icon);
|
| 187 |
+
builder.AddAttribute(2, "class", "h-3.5 w-3.5 mr-3 text-slate-400 shrink-0");
|
| 188 |
+
builder.CloseElement();
|
| 189 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 192 |
{
|
|
|
|
| 196 |
}
|
| 197 |
catch
|
| 198 |
{
|
|
|
|
| 199 |
}
|
| 200 |
}
|
| 201 |
|
|
|
|
| 203 |
{
|
| 204 |
Navigation.LocationChanged -= OnLocationChanged;
|
| 205 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/Login.razor
CHANGED
|
@@ -19,6 +19,9 @@
|
|
| 19 |
Or
|
| 20 |
<a href="/register" class="font-medium text-slate-900 hover:underline">register a new account</a>
|
| 21 |
</p>
|
|
|
|
|
|
|
|
|
|
| 22 |
</div>
|
| 23 |
|
| 24 |
<form method="post" action="/account/login" class="mt-8 space-y-6">
|
|
@@ -68,6 +71,12 @@
|
|
| 68 |
<p class="text-sm text-emerald-700 text-center">You have been signed out successfully.</p>
|
| 69 |
</div>
|
| 70 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
<div>
|
| 73 |
<button type="submit" class="group relative flex w-full justify-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">
|
|
@@ -88,9 +97,13 @@
|
|
| 88 |
[SupplyParameterFromQuery(Name = "loggedOut")]
|
| 89 |
private string? LoggedOut { get; set; }
|
| 90 |
|
|
|
|
|
|
|
|
|
|
| 91 |
private bool ShowError => string.Equals(Error, "invalid", StringComparison.OrdinalIgnoreCase);
|
| 92 |
private bool ShowUnavailableError => string.Equals(Error, "unavailable", StringComparison.OrdinalIgnoreCase);
|
| 93 |
private bool ShowLoggedOutMessage => string.Equals(LoggedOut, "true", StringComparison.OrdinalIgnoreCase);
|
|
|
|
| 94 |
|
| 95 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 96 |
{
|
|
|
|
| 19 |
Or
|
| 20 |
<a href="/register" class="font-medium text-slate-900 hover:underline">register a new account</a>
|
| 21 |
</p>
|
| 22 |
+
<p class="mt-2 text-center text-sm text-slate-600">
|
| 23 |
+
<a href="/reset-password" class="font-medium text-violet-600 hover:underline">reset your password</a>
|
| 24 |
+
</p>
|
| 25 |
</div>
|
| 26 |
|
| 27 |
<form method="post" action="/account/login" class="mt-8 space-y-6">
|
|
|
|
| 71 |
<p class="text-sm text-emerald-700 text-center">You have been signed out successfully.</p>
|
| 72 |
</div>
|
| 73 |
}
|
| 74 |
+
else if (ShowResetMessage)
|
| 75 |
+
{
|
| 76 |
+
<div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
|
| 77 |
+
<p class="text-sm text-emerald-700 text-center">Password updated successfully. Please sign in with your new password.</p>
|
| 78 |
+
</div>
|
| 79 |
+
}
|
| 80 |
|
| 81 |
<div>
|
| 82 |
<button type="submit" class="group relative flex w-full justify-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">
|
|
|
|
| 97 |
[SupplyParameterFromQuery(Name = "loggedOut")]
|
| 98 |
private string? LoggedOut { get; set; }
|
| 99 |
|
| 100 |
+
[SupplyParameterFromQuery(Name = "reset")]
|
| 101 |
+
private string? Reset { get; set; }
|
| 102 |
+
|
| 103 |
private bool ShowError => string.Equals(Error, "invalid", StringComparison.OrdinalIgnoreCase);
|
| 104 |
private bool ShowUnavailableError => string.Equals(Error, "unavailable", StringComparison.OrdinalIgnoreCase);
|
| 105 |
private bool ShowLoggedOutMessage => string.Equals(LoggedOut, "true", StringComparison.OrdinalIgnoreCase);
|
| 106 |
+
private bool ShowResetMessage => string.Equals(Reset, "true", StringComparison.OrdinalIgnoreCase);
|
| 107 |
|
| 108 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 109 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/Projects.razor
CHANGED
|
@@ -391,23 +391,7 @@
|
|
| 391 |
var client = ApiClient.CreateClient();
|
| 392 |
try
|
| 393 |
{
|
| 394 |
-
|
| 395 |
-
if (!isAdmin && currentUserId > 0)
|
| 396 |
-
{
|
| 397 |
-
// Non-admin: only show projects they're a member of
|
| 398 |
-
var memberProjectIds = new HashSet<long>();
|
| 399 |
-
foreach (var p in all)
|
| 400 |
-
{
|
| 401 |
-
var membersResult = await client.GetFromJsonAsync<Result<IEnumerable<UserDto>>>($"Project/{p.Id}/members", Serialization.CaseInsensitive);
|
| 402 |
-
if (membersResult?.Value?.Any(m => m.Id == currentUserId) == true)
|
| 403 |
-
memberProjectIds.Add(p.Id);
|
| 404 |
-
}
|
| 405 |
-
projects = all.Where(p => memberProjectIds.Contains(p.Id)).ToList();
|
| 406 |
-
}
|
| 407 |
-
else
|
| 408 |
-
{
|
| 409 |
-
projects = all;
|
| 410 |
-
}
|
| 411 |
}
|
| 412 |
catch (Exception ex)
|
| 413 |
{
|
|
|
|
| 391 |
var client = ApiClient.CreateClient();
|
| 392 |
try
|
| 393 |
{
|
| 394 |
+
projects = await client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive) ?? new();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
}
|
| 396 |
catch (Exception ex)
|
| 397 |
{
|
TaskTrackingSystem.WebApp/Components/Pages/Reports.razor
DELETED
|
@@ -1,292 +0,0 @@
|
|
| 1 |
-
@page "/reports"
|
| 2 |
-
@using Microsoft.AspNetCore.Authorization
|
| 3 |
-
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 4 |
-
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
| 5 |
-
@inject ApiClientService ApiClient
|
| 6 |
-
@inject IJSRuntime JS
|
| 7 |
-
@inject AuthenticationStateProvider AuthStateProvider
|
| 8 |
-
@inject MenuAuthorizationService MenuAuthorization
|
| 9 |
-
@inject NavigationManager Navigation
|
| 10 |
-
|
| 11 |
-
<PageTitle>Reports</PageTitle>
|
| 12 |
-
|
| 13 |
-
<div class="space-y-8">
|
| 14 |
-
<!-- Page Header -->
|
| 15 |
-
<div>
|
| 16 |
-
<h2 class="text-3xl font-bold tracking-tight text-slate-900">Reports</h2>
|
| 17 |
-
<p class="text-slate-500 mt-1">Analytics, task reports, and user productivity.</p>
|
| 18 |
-
</div>
|
| 19 |
-
|
| 20 |
-
<!-- Task Report Section -->
|
| 21 |
-
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 22 |
-
<div class="px-6 py-4 border-b border-slate-100">
|
| 23 |
-
<h3 class="font-semibold text-lg text-slate-900">Task Report</h3>
|
| 24 |
-
</div>
|
| 25 |
-
|
| 26 |
-
<!-- Filters -->
|
| 27 |
-
<div class="px-6 py-4 bg-slate-50/50 border-b border-slate-100">
|
| 28 |
-
<div class="flex flex-wrap items-end gap-4">
|
| 29 |
-
<div>
|
| 30 |
-
<label class="block text-xs font-medium text-slate-500 mb-1">Start Date</label>
|
| 31 |
-
<input @bind="filterStartDate" type="date" class="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
|
| 32 |
-
</div>
|
| 33 |
-
<div>
|
| 34 |
-
<label class="block text-xs font-medium text-slate-500 mb-1">End Date</label>
|
| 35 |
-
<input @bind="filterEndDate" type="date" class="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
|
| 36 |
-
</div>
|
| 37 |
-
<div>
|
| 38 |
-
<label class="block text-xs font-medium text-slate-500 mb-1">Status</label>
|
| 39 |
-
<select @bind="filterStatus" class="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
|
| 40 |
-
<option value="">All</option>
|
| 41 |
-
<option value="1">To Do</option>
|
| 42 |
-
<option value="2">In Progress</option>
|
| 43 |
-
<option value="3">Done</option>
|
| 44 |
-
</select>
|
| 45 |
-
</div>
|
| 46 |
-
<div>
|
| 47 |
-
<label class="block text-xs font-medium text-slate-500 mb-1">Project</label>
|
| 48 |
-
<select @bind="filterProjectId" class="rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
|
| 49 |
-
<option value="0">All Projects</option>
|
| 50 |
-
@foreach (var p in projects)
|
| 51 |
-
{
|
| 52 |
-
<option value="@p.Id">@p.Name</option>
|
| 53 |
-
}
|
| 54 |
-
</select>
|
| 55 |
-
</div>
|
| 56 |
-
<button @onclick="LoadTaskReport" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors">
|
| 57 |
-
<i data-lucide="search" class="h-3.5 w-3.5 mr-1.5"></i>
|
| 58 |
-
Search
|
| 59 |
-
</button>
|
| 60 |
-
<button @onclick="ResetFilters" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors">
|
| 61 |
-
<i data-lucide="rotate-ccw" class="h-3.5 w-3.5 mr-1.5 text-slate-500"></i>
|
| 62 |
-
Reset
|
| 63 |
-
</button>
|
| 64 |
-
</div>
|
| 65 |
-
</div>
|
| 66 |
-
|
| 67 |
-
<!-- Task Report Table -->
|
| 68 |
-
<div class="overflow-x-auto">
|
| 69 |
-
@if (isLoadingTasks)
|
| 70 |
-
{
|
| 71 |
-
<div class="flex items-center justify-center py-12">
|
| 72 |
-
<svg class="animate-spin h-5 w-5 text-slate-400" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
| 73 |
-
</div>
|
| 74 |
-
}
|
| 75 |
-
else
|
| 76 |
-
{
|
| 77 |
-
<table class="w-full">
|
| 78 |
-
<thead>
|
| 79 |
-
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 80 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Task</th>
|
| 81 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Project</th>
|
| 82 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
| 83 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Priority</th>
|
| 84 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Assigned To</th>
|
| 85 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Due Date</th>
|
| 86 |
-
</tr>
|
| 87 |
-
</thead>
|
| 88 |
-
<tbody class="divide-y divide-slate-100">
|
| 89 |
-
@if (taskReports.Any())
|
| 90 |
-
{
|
| 91 |
-
@foreach (var t in taskReports)
|
| 92 |
-
{
|
| 93 |
-
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 94 |
-
<td class="px-6 py-3 text-sm font-medium text-slate-900">@t.Title</td>
|
| 95 |
-
<td class="px-6 py-3 text-sm text-slate-500">@t.ProjectName</td>
|
| 96 |
-
<td class="px-6 py-3">
|
| 97 |
-
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetStatusBadge(t.StatusId)">@t.StatusName</span>
|
| 98 |
-
</td>
|
| 99 |
-
<td class="px-6 py-3">
|
| 100 |
-
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium @GetPriorityBadge(t.PriorityId)">@t.PriorityName</span>
|
| 101 |
-
</td>
|
| 102 |
-
<td class="px-6 py-3 text-sm text-slate-500">@(t.AssignedToUser ?? "Unassigned")</td>
|
| 103 |
-
<td class="px-6 py-3 text-sm text-slate-500">@t.DueDate.ToString("dd-MMM-yyyy")</td>
|
| 104 |
-
</tr>
|
| 105 |
-
}
|
| 106 |
-
}
|
| 107 |
-
else
|
| 108 |
-
{
|
| 109 |
-
<tr>
|
| 110 |
-
<td colspan="6" class="px-6 py-12 text-center">
|
| 111 |
-
<i data-lucide="file-search" class="h-10 w-10 mx-auto text-slate-300 mb-3"></i>
|
| 112 |
-
<p class="text-sm text-slate-400">No tasks found. Adjust filters and try again.</p>
|
| 113 |
-
</td>
|
| 114 |
-
</tr>
|
| 115 |
-
}
|
| 116 |
-
</tbody>
|
| 117 |
-
</table>
|
| 118 |
-
}
|
| 119 |
-
</div>
|
| 120 |
-
</div>
|
| 121 |
-
|
| 122 |
-
<!-- User Productivity Section -->
|
| 123 |
-
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
|
| 124 |
-
<div class="px-6 py-4 border-b border-slate-100">
|
| 125 |
-
<h3 class="font-semibold text-lg text-slate-900">User Productivity</h3>
|
| 126 |
-
</div>
|
| 127 |
-
|
| 128 |
-
@if (isLoadingProductivity)
|
| 129 |
-
{
|
| 130 |
-
<div class="flex items-center justify-center py-12">
|
| 131 |
-
<svg class="animate-spin h-5 w-5 text-slate-400" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
| 132 |
-
</div>
|
| 133 |
-
}
|
| 134 |
-
else
|
| 135 |
-
{
|
| 136 |
-
<table class="w-full">
|
| 137 |
-
<thead>
|
| 138 |
-
<tr class="border-b border-slate-100 bg-slate-50/60">
|
| 139 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">User</th>
|
| 140 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Total Assigned</th>
|
| 141 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Completed</th>
|
| 142 |
-
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Efficiency</th>
|
| 143 |
-
</tr>
|
| 144 |
-
</thead>
|
| 145 |
-
<tbody class="divide-y divide-slate-100">
|
| 146 |
-
@if (productivityList.Any())
|
| 147 |
-
{
|
| 148 |
-
@foreach (var u in productivityList)
|
| 149 |
-
{
|
| 150 |
-
<tr class="hover:bg-slate-50/50 transition-colors">
|
| 151 |
-
<td class="px-6 py-4">
|
| 152 |
-
<div class="flex items-center space-x-3">
|
| 153 |
-
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
|
| 154 |
-
@(u.FullName.Length > 0 ? u.FullName[0].ToString().ToUpper() : "U")
|
| 155 |
-
</span>
|
| 156 |
-
<div>
|
| 157 |
-
<p class="text-sm font-semibold text-slate-900">@u.FullName</p>
|
| 158 |
-
<p class="text-xs text-slate-400">@@@u.Username</p>
|
| 159 |
-
</div>
|
| 160 |
-
</div>
|
| 161 |
-
</td>
|
| 162 |
-
<td class="px-6 py-4 text-sm font-medium text-slate-900">@u.TotalAssignedTasks</td>
|
| 163 |
-
<td class="px-6 py-4 text-sm font-medium text-emerald-600">@u.CompletedTasksCount</td>
|
| 164 |
-
<td class="px-6 py-4">
|
| 165 |
-
<div class="flex items-center space-x-3">
|
| 166 |
-
<div class="flex-1 h-2 rounded-full bg-slate-100 overflow-hidden max-w-[120px]">
|
| 167 |
-
<div class="h-full rounded-full bg-slate-900 transition-all" style="width: @(Math.Min(u.EfficiencyRatio, 100))%"></div>
|
| 168 |
-
</div>
|
| 169 |
-
<span class="text-sm font-medium text-slate-700">@u.EfficiencyRatio.ToString("F0")%</span>
|
| 170 |
-
</div>
|
| 171 |
-
</td>
|
| 172 |
-
</tr>
|
| 173 |
-
}
|
| 174 |
-
}
|
| 175 |
-
else
|
| 176 |
-
{
|
| 177 |
-
<tr>
|
| 178 |
-
<td colspan="4" class="px-6 py-12 text-center">
|
| 179 |
-
<p class="text-sm text-slate-400">No productivity data available.</p>
|
| 180 |
-
</td>
|
| 181 |
-
</tr>
|
| 182 |
-
}
|
| 183 |
-
</tbody>
|
| 184 |
-
</table>
|
| 185 |
-
}
|
| 186 |
-
</div>
|
| 187 |
-
</div>
|
| 188 |
-
|
| 189 |
-
@code {
|
| 190 |
-
private bool isLoadingTasks = true;
|
| 191 |
-
private bool isLoadingProductivity = true;
|
| 192 |
-
private List<TaskReportDto> taskReports = new();
|
| 193 |
-
private List<UserProductivityDto> productivityList = new();
|
| 194 |
-
private List<ProjectDto> projects = new();
|
| 195 |
-
|
| 196 |
-
// Filters
|
| 197 |
-
private DateTime? filterStartDate;
|
| 198 |
-
private DateTime? filterEndDate;
|
| 199 |
-
private string? filterStatus;
|
| 200 |
-
private int filterProjectId;
|
| 201 |
-
|
| 202 |
-
protected override async Task OnInitializedAsync()
|
| 203 |
-
{
|
| 204 |
-
var client = ApiClient.CreateClient();
|
| 205 |
-
try
|
| 206 |
-
{
|
| 207 |
-
projects = await client.GetFromJsonAsync<List<ProjectDto>>("Project", Serialization.CaseInsensitive) ?? new();
|
| 208 |
-
}
|
| 209 |
-
catch { }
|
| 210 |
-
|
| 211 |
-
await System.Threading.Tasks.Task.WhenAll(LoadTaskReport(), LoadProductivity());
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
protected override async Task OnAfterRenderAsync(bool firstRender)
|
| 215 |
-
{
|
| 216 |
-
await JS.InvokeVoidAsync("initIcons");
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
private async Task LoadTaskReport()
|
| 220 |
-
{
|
| 221 |
-
isLoadingTasks = true;
|
| 222 |
-
StateHasChanged();
|
| 223 |
-
|
| 224 |
-
var client = ApiClient.CreateClient();
|
| 225 |
-
try
|
| 226 |
-
{
|
| 227 |
-
var query = "Report/tasks?";
|
| 228 |
-
if (filterStartDate.HasValue) query += $"startDate={filterStartDate:yyyy-MM-dd}&";
|
| 229 |
-
if (filterEndDate.HasValue) query += $"endDate={filterEndDate:yyyy-MM-dd}&";
|
| 230 |
-
if (!string.IsNullOrEmpty(filterStatus)) query += $"status={filterStatus}&";
|
| 231 |
-
if (filterProjectId > 0) query += $"projectId={filterProjectId}&";
|
| 232 |
-
|
| 233 |
-
var result = await client.GetFromJsonAsync<Result<List<TaskReportDto>>>(query.TrimEnd('&', '?'), Serialization.CaseInsensitive);
|
| 234 |
-
if (result?.IsSuccess == true && result.Value != null)
|
| 235 |
-
taskReports = result.Value;
|
| 236 |
-
}
|
| 237 |
-
catch (Exception ex)
|
| 238 |
-
{
|
| 239 |
-
Console.WriteLine($"Task report error: {ex.Message}");
|
| 240 |
-
}
|
| 241 |
-
finally
|
| 242 |
-
{
|
| 243 |
-
isLoadingTasks = false;
|
| 244 |
-
StateHasChanged();
|
| 245 |
-
}
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
private async Task LoadProductivity()
|
| 249 |
-
{
|
| 250 |
-
isLoadingProductivity = true;
|
| 251 |
-
var client = ApiClient.CreateClient();
|
| 252 |
-
try
|
| 253 |
-
{
|
| 254 |
-
var result = await client.GetFromJsonAsync<Result<List<UserProductivityDto>>>("Report/user-productivity", Serialization.CaseInsensitive);
|
| 255 |
-
if (result?.IsSuccess == true && result.Value != null)
|
| 256 |
-
productivityList = result.Value;
|
| 257 |
-
}
|
| 258 |
-
catch (Exception ex)
|
| 259 |
-
{
|
| 260 |
-
Console.WriteLine($"Productivity error: {ex.Message}");
|
| 261 |
-
}
|
| 262 |
-
finally
|
| 263 |
-
{
|
| 264 |
-
isLoadingProductivity = false;
|
| 265 |
-
}
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
private async Task ResetFilters()
|
| 269 |
-
{
|
| 270 |
-
filterStartDate = null;
|
| 271 |
-
filterEndDate = null;
|
| 272 |
-
filterStatus = null;
|
| 273 |
-
filterProjectId = 0;
|
| 274 |
-
await LoadTaskReport();
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
private string GetStatusBadge(long statusId) => statusId switch
|
| 278 |
-
{
|
| 279 |
-
1 => "bg-slate-100 text-slate-700",
|
| 280 |
-
2 => "bg-blue-100 text-blue-700",
|
| 281 |
-
3 => "bg-emerald-100 text-emerald-700",
|
| 282 |
-
_ => "bg-slate-100 text-slate-600"
|
| 283 |
-
};
|
| 284 |
-
|
| 285 |
-
private string GetPriorityBadge(long priorityId) => priorityId switch
|
| 286 |
-
{
|
| 287 |
-
1 => "bg-slate-100 text-slate-600",
|
| 288 |
-
2 => "bg-amber-100 text-amber-700",
|
| 289 |
-
3 => "bg-red-100 text-red-700",
|
| 290 |
-
_ => "bg-slate-100 text-slate-600"
|
| 291 |
-
};
|
| 292 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.WebApp/Components/Pages/ResetPassword.razor
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@page "/reset-password"
|
| 2 |
+
@layout Layout.EmptyLayout
|
| 3 |
+
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
| 4 |
+
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
| 5 |
+
@inject NavigationManager Navigation
|
| 6 |
+
|
| 7 |
+
<PageTitle>Reset Password - TaskFlow</PageTitle>
|
| 8 |
+
|
| 9 |
+
<div class="flex min-h-screen items-center justify-center bg-slate-50 py-12 px-4 sm:px-6 lg:px-8">
|
| 10 |
+
<div class="w-full max-w-md space-y-8 bg-white p-8 rounded-2xl border border-slate-200 shadow-xl">
|
| 11 |
+
<div>
|
| 12 |
+
<div class="flex justify-center">
|
| 13 |
+
<span class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-600 text-white font-bold text-xl">T</span>
|
| 14 |
+
</div>
|
| 15 |
+
<h2 class="mt-6 text-center text-3xl font-extrabold text-slate-900">Reset your password</h2>
|
| 16 |
+
<p class="mt-2 text-center text-sm text-slate-600">
|
| 17 |
+
Use the recovery code configured in the API to set a new password.
|
| 18 |
+
</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<form method="post" action="/account/reset-password" class="mt-8 space-y-6">
|
| 22 |
+
<AntiforgeryToken />
|
| 23 |
+
|
| 24 |
+
<div class="space-y-4 rounded-md shadow-sm">
|
| 25 |
+
<div>
|
| 26 |
+
<label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">Username or Email</label>
|
| 27 |
+
<input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="Enter username or email"
|
| 28 |
+
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
|
| 29 |
+
</div>
|
| 30 |
+
<div>
|
| 31 |
+
<label for="recoveryCode" class="block text-sm font-medium text-slate-700 mb-1">Recovery Code</label>
|
| 32 |
+
<input id="recoveryCode" name="RecoveryCode" required placeholder="Enter recovery code"
|
| 33 |
+
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
|
| 34 |
+
</div>
|
| 35 |
+
<div>
|
| 36 |
+
<label for="newPassword" class="block text-sm font-medium text-slate-700 mb-1">New Password</label>
|
| 37 |
+
<input id="newPassword" name="NewPassword" type="password" required placeholder="Min 8 characters"
|
| 38 |
+
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
@if (ShowError)
|
| 43 |
+
{
|
| 44 |
+
<div class="p-3 rounded-lg bg-red-50 border border-red-100">
|
| 45 |
+
<p class="text-sm text-red-600 text-center">@ErrorMessage</p>
|
| 46 |
+
</div>
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
<div class="flex items-center justify-between">
|
| 50 |
+
<a href="/login" class="text-sm font-medium text-violet-600 hover:underline">Back to sign in</a>
|
| 51 |
+
<button type="submit" class="group relative flex justify-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">
|
| 52 |
+
Reset Password
|
| 53 |
+
</button>
|
| 54 |
+
</div>
|
| 55 |
+
</form>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
@code {
|
| 60 |
+
[SupplyParameterFromQuery(Name = "error")]
|
| 61 |
+
private string? Error { get; set; }
|
| 62 |
+
|
| 63 |
+
private bool ShowError => !string.IsNullOrWhiteSpace(Error);
|
| 64 |
+
private string ErrorMessage => string.IsNullOrWhiteSpace(Error) ? "Unable to reset password." : Error;
|
| 65 |
+
}
|
TaskTrackingSystem.WebApp/MenuAuthorizationService.cs
CHANGED
|
@@ -11,16 +11,8 @@ public class MenuAuthorizationService
|
|
| 11 |
string.Empty,
|
| 12 |
"login",
|
| 13 |
"register",
|
| 14 |
-
"error"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
private static readonly HashSet<string> MemberRouteSegments = new(StringComparer.OrdinalIgnoreCase)
|
| 18 |
-
{
|
| 19 |
-
string.Empty,
|
| 20 |
-
"dashboard",
|
| 21 |
-
"home",
|
| 22 |
-
"projects",
|
| 23 |
-
"tasks"
|
| 24 |
};
|
| 25 |
|
| 26 |
private readonly ApiClientService _apiClient;
|
|
@@ -71,14 +63,14 @@ public class MenuAuthorizationService
|
|
| 71 |
return true;
|
| 72 |
}
|
| 73 |
|
| 74 |
-
var
|
| 75 |
|
| 76 |
-
if (_sessionState.
|
| 77 |
{
|
| 78 |
return IsRouteAllowed(_sessionState.CachedMenus, relativePath);
|
| 79 |
}
|
| 80 |
|
| 81 |
-
return
|
| 82 |
}
|
| 83 |
|
| 84 |
public bool IsRouteAllowed(IReadOnlyList<MenuDto> menus, string relativePath)
|
|
@@ -106,14 +98,14 @@ public class MenuAuthorizationService
|
|
| 106 |
return Task.FromResult(new List<MenuDto>());
|
| 107 |
}
|
| 108 |
|
| 109 |
-
var
|
| 110 |
|
| 111 |
-
if (_sessionState.
|
| 112 |
{
|
| 113 |
return Task.FromResult(_sessionState.CachedMenus);
|
| 114 |
}
|
| 115 |
|
| 116 |
-
return LoadMenusAsync(user,
|
| 117 |
}
|
| 118 |
|
| 119 |
public void PreloadMenus(ClaimsPrincipal user)
|
|
@@ -123,27 +115,27 @@ public class MenuAuthorizationService
|
|
| 123 |
return;
|
| 124 |
}
|
| 125 |
|
| 126 |
-
var
|
| 127 |
-
if (_sessionState.
|
| 128 |
{
|
| 129 |
return;
|
| 130 |
}
|
| 131 |
|
| 132 |
-
_ = LoadMenusAsync(user,
|
| 133 |
}
|
| 134 |
|
| 135 |
-
private Task<List<MenuDto>> LoadMenusAsync(ClaimsPrincipal user, string
|
| 136 |
{
|
| 137 |
if (_loadingMenus is { IsCompleted: false })
|
| 138 |
{
|
| 139 |
return _loadingMenus;
|
| 140 |
}
|
| 141 |
|
| 142 |
-
_loadingMenus = FetchMenusFromApiAsync(user,
|
| 143 |
return _loadingMenus;
|
| 144 |
}
|
| 145 |
|
| 146 |
-
private async Task<List<MenuDto>> FetchMenusFromApiAsync(ClaimsPrincipal user, string
|
| 147 |
{
|
| 148 |
try
|
| 149 |
{
|
|
@@ -159,14 +151,22 @@ public class MenuAuthorizationService
|
|
| 159 |
var menus = await response.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
|
| 160 |
?? new List<MenuDto>();
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
_sessionState.CachedMenus = menus;
|
| 163 |
-
_sessionState.
|
| 164 |
return menus;
|
| 165 |
}
|
| 166 |
catch (Exception ex)
|
| 167 |
{
|
| 168 |
Console.WriteLine($"Menu API error: {ex.Message}");
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
| 170 |
}
|
| 171 |
finally
|
| 172 |
{
|
|
@@ -174,28 +174,6 @@ public class MenuAuthorizationService
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
|
| 177 |
-
private static bool IsRouteAllowedByRole(string role, string relativePath)
|
| 178 |
-
{
|
| 179 |
-
var segment = GetFirstSegment(relativePath);
|
| 180 |
-
|
| 181 |
-
if (PublicRouteSegments.Contains(segment))
|
| 182 |
-
{
|
| 183 |
-
return true;
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
if (role.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 187 |
-
{
|
| 188 |
-
return true;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
if (role.Equals("Member", StringComparison.OrdinalIgnoreCase))
|
| 192 |
-
{
|
| 193 |
-
return MemberRouteSegments.Contains(segment);
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
return MemberRouteSegments.Contains(segment);
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
private static bool MenuCollectionMatchesSegment(IReadOnlyList<MenuDto> menus, string segment)
|
| 200 |
{
|
| 201 |
foreach (var menu in menus)
|
|
@@ -227,4 +205,65 @@ public class MenuAuthorizationService
|
|
| 227 |
var menuSegment = menu.MenuUrl.Split('?')[0].Trim('/').Split('/')[0];
|
| 228 |
return menuSegment.Equals(segment, StringComparison.OrdinalIgnoreCase);
|
| 229 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
|
|
|
| 11 |
string.Empty,
|
| 12 |
"login",
|
| 13 |
"register",
|
| 14 |
+
"error",
|
| 15 |
+
"reset-password"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
};
|
| 17 |
|
| 18 |
private readonly ApiClientService _apiClient;
|
|
|
|
| 63 |
return true;
|
| 64 |
}
|
| 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)
|
|
|
|
| 98 |
return Task.FromResult(new List<MenuDto>());
|
| 99 |
}
|
| 100 |
|
| 101 |
+
var roleKey = GetRoleCacheKey(user);
|
| 102 |
|
| 103 |
+
if (_sessionState.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
|
| 104 |
{
|
| 105 |
return Task.FromResult(_sessionState.CachedMenus);
|
| 106 |
}
|
| 107 |
|
| 108 |
+
return LoadMenusAsync(user, roleKey);
|
| 109 |
}
|
| 110 |
|
| 111 |
public void PreloadMenus(ClaimsPrincipal user)
|
|
|
|
| 115 |
return;
|
| 116 |
}
|
| 117 |
|
| 118 |
+
var roleKey = GetRoleCacheKey(user);
|
| 119 |
+
if (_sessionState.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
|
| 120 |
{
|
| 121 |
return;
|
| 122 |
}
|
| 123 |
|
| 124 |
+
_ = LoadMenusAsync(user, roleKey);
|
| 125 |
}
|
| 126 |
|
| 127 |
+
private Task<List<MenuDto>> LoadMenusAsync(ClaimsPrincipal user, string roleKey)
|
| 128 |
{
|
| 129 |
if (_loadingMenus is { IsCompleted: false })
|
| 130 |
{
|
| 131 |
return _loadingMenus;
|
| 132 |
}
|
| 133 |
|
| 134 |
+
_loadingMenus = FetchMenusFromApiAsync(user, roleKey);
|
| 135 |
return _loadingMenus;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
private async Task<List<MenuDto>> FetchMenusFromApiAsync(ClaimsPrincipal user, string roleKey)
|
| 139 |
{
|
| 140 |
try
|
| 141 |
{
|
|
|
|
| 151 |
var menus = await response.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
|
| 152 |
?? new List<MenuDto>();
|
| 153 |
|
| 154 |
+
if (menus.Count == 0)
|
| 155 |
+
{
|
| 156 |
+
menus = BuildFallbackMenus(user);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
_sessionState.CachedMenus = menus;
|
| 160 |
+
_sessionState.CachedMenuRoleId = roleKey;
|
| 161 |
return menus;
|
| 162 |
}
|
| 163 |
catch (Exception ex)
|
| 164 |
{
|
| 165 |
Console.WriteLine($"Menu API error: {ex.Message}");
|
| 166 |
+
var fallbackMenus = BuildFallbackMenus(user);
|
| 167 |
+
_sessionState.CachedMenus = fallbackMenus;
|
| 168 |
+
_sessionState.CachedMenuRoleId = roleKey;
|
| 169 |
+
return fallbackMenus;
|
| 170 |
}
|
| 171 |
finally
|
| 172 |
{
|
|
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
private static bool MenuCollectionMatchesSegment(IReadOnlyList<MenuDto> menus, string segment)
|
| 178 |
{
|
| 179 |
foreach (var menu in menus)
|
|
|
|
| 205 |
var menuSegment = menu.MenuUrl.Split('?')[0].Trim('/').Split('/')[0];
|
| 206 |
return menuSegment.Equals(segment, StringComparison.OrdinalIgnoreCase);
|
| 207 |
}
|
| 208 |
+
|
| 209 |
+
private static string GetRoleCacheKey(ClaimsPrincipal user)
|
| 210 |
+
{
|
| 211 |
+
var roleId = user.FindFirst("role_id")?.Value;
|
| 212 |
+
if (!string.IsNullOrWhiteSpace(roleId))
|
| 213 |
+
{
|
| 214 |
+
return $"id:{roleId}";
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
var roleName = user.FindFirst(ClaimTypes.Role)?.Value;
|
| 218 |
+
if (!string.IsNullOrWhiteSpace(roleName))
|
| 219 |
+
{
|
| 220 |
+
return $"name:{roleName}";
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
return string.Empty;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
private static List<MenuDto> BuildFallbackMenus(ClaimsPrincipal user)
|
| 227 |
+
{
|
| 228 |
+
var roleName = user.FindFirst(ClaimTypes.Role)?.Value ?? string.Empty;
|
| 229 |
+
|
| 230 |
+
var menus = new List<MenuDto>
|
| 231 |
+
{
|
| 232 |
+
CreateMenu("dashboard", "Dashboard", "/dashboard", 1, "layout-dashboard")
|
| 233 |
+
};
|
| 234 |
+
|
| 235 |
+
if (roleName.Equals("Employee", StringComparison.OrdinalIgnoreCase))
|
| 236 |
+
{
|
| 237 |
+
menus.Add(CreateMenu("tasks", "Tasks", "/tasks", 2, "list-checks"));
|
| 238 |
+
return menus;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
menus.Add(CreateMenu("tasks", "Tasks", "/tasks", 2, "list-checks"));
|
| 242 |
+
menus.Add(CreateMenu("projects", "Projects", "/projects", 3, "folder-kanban"));
|
| 243 |
+
menus.Add(CreateMenu("reports", "Reports", "/reports/tasks", 4, "bar-chart-3"));
|
| 244 |
+
|
| 245 |
+
if (roleName.Equals("Manager", StringComparison.OrdinalIgnoreCase) ||
|
| 246 |
+
roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 247 |
+
{
|
| 248 |
+
if (roleName.Equals("Admin", StringComparison.OrdinalIgnoreCase))
|
| 249 |
+
{
|
| 250 |
+
menus.Add(CreateMenu("users", "Users", "/users", 5, "users"));
|
| 251 |
+
menus.Add(CreateMenu("roles", "Roles", "/roles", 6, "shield"));
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
return menus;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
private static MenuDto CreateMenu(string code, string name, string url, int orderNo, string icon)
|
| 259 |
+
{
|
| 260 |
+
return new MenuDto
|
| 261 |
+
{
|
| 262 |
+
MenuCode = code,
|
| 263 |
+
MenuName = name,
|
| 264 |
+
MenuUrl = url,
|
| 265 |
+
OrderNo = orderNo,
|
| 266 |
+
Icon = icon
|
| 267 |
+
};
|
| 268 |
+
}
|
| 269 |
}
|
TaskTrackingSystem.WebApp/UserSessionState.cs
CHANGED
|
@@ -8,12 +8,12 @@ public class UserSessionState
|
|
| 8 |
public string? Token { get; set; }
|
| 9 |
public ClaimsPrincipal? CachedUser { get; set; }
|
| 10 |
|
| 11 |
-
public string?
|
| 12 |
public List<MenuDto>? CachedMenus { get; set; }
|
| 13 |
|
| 14 |
public void ClearMenuCache()
|
| 15 |
{
|
| 16 |
-
|
| 17 |
CachedMenus = null;
|
| 18 |
}
|
| 19 |
|
|
|
|
| 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 |
public void ClearMenuCache()
|
| 15 |
{
|
| 16 |
+
CachedMenuRoleId = null;
|
| 17 |
CachedMenus = null;
|
| 18 |
}
|
| 19 |
|