Initial commit: HelpDesk ICHN - .NET 10 backend + Next.js 16 frontend

This commit is contained in:
Nurfog committed 2026-09-10 10:20:51 -03:00
commit 359f0caf85
90 files changed
+13395

No files matched your search

@@ -0,0 +1,115 @@
using System.Security.Claims;
using HelpDesk.API.Services;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using HelpDesk.Models.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IUserRepository _userRepository;
private readonly IJwtService _jwtService;
private readonly IPasswordService _passwordService;
public AuthController(IUserRepository userRepository, IJwtService jwtService, IPasswordService passwordService)
{
_userRepository = userRepository;
_jwtService = jwtService;
_passwordService = passwordService;
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login([FromBody] LoginRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
return BadRequest(new { error = "Email and password are required." });
var user = await _userRepository.GetByEmailAsync(request.Email);
if (user == null || !_passwordService.VerifyPassword(request.Password, user.PasswordHash))
return Unauthorized(new { error = "Invalid email or password." });
if (!user.IsActive)
return Unauthorized(new { error = "Account is deactivated." });
var token = _jwtService.GenerateToken(user.Id, user.Email, user.Role);
return Ok(new AuthResponse
{
Token = token,
Expiration = DateTime.UtcNow.AddMinutes(60),
User = MapToDto(user)
});
}
[HttpPost("register")]
public async Task<ActionResult<AuthResponse>> Register([FromBody] RegisterRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
return BadRequest(new { error = "Email and password are required." });
if (string.IsNullOrWhiteSpace(request.FirstName) || string.IsNullOrWhiteSpace(request.LastName))
return BadRequest(new { error = "First name and last name are required." });
if (request.Password.Length < 6)
return BadRequest(new { error = "Password must be at least 6 characters." });
if (await _userRepository.ExistsAsync(request.Email))
return Conflict(new { error = "Email is already registered." });
var user = new User
{
Email = request.Email,
PasswordHash = _passwordService.HashPassword(request.Password),
FirstName = request.FirstName,
LastName = request.LastName,
Role = UserRole.Customer,
CreatedAt = DateTime.UtcNow
};
var created = await _userRepository.CreateAsync(user);
var token = _jwtService.GenerateToken(created.Id, created.Email, created.Role);
return CreatedAtAction(nameof(GetMe), new AuthResponse
{
Token = token,
Expiration = DateTime.UtcNow.AddMinutes(60),
User = MapToDto(created)
});
}
[Authorize]
[HttpGet("me")]
public async Task<ActionResult<UserDto>> GetMe()
{
var userId = GetUserId();
if (userId == null) return Unauthorized();
var user = await _userRepository.GetByIdAsync(userId.Value);
if (user == null) return NotFound(new { error = "User not found." });
return Ok(MapToDto(user));
}
private Guid? GetUserId()
{
var claim = User.FindFirst("userId") ?? User.FindFirst(ClaimTypes.NameIdentifier);
return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null;
}
private static UserDto MapToDto(User user) => new()
{
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
Role = user.Role,
IsActive = user.IsActive,
CreatedAt = user.CreatedAt,
PhoneNumber = user.PhoneNumber,
AvatarUrl = user.AvatarUrl
};
}
@@ -0,0 +1,67 @@
using System.Security.Claims;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using HelpDesk.Models.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "Agent")]
public class DashboardController : ControllerBase
{
private readonly ITicketRepository _ticketRepository;
private readonly IUserRepository _userRepository;
public DashboardController(ITicketRepository ticketRepository, IUserRepository userRepository)
{
_ticketRepository = ticketRepository;
_userRepository = userRepository;
}
[HttpGet("stats")]
public async Task<ActionResult<DashboardStatsDto>> GetStats()
{
var agents = await _userRepository.GetByRoleAsync(UserRole.Agent);
var agentWorkloads = new List<AgentWorkloadDto>();
foreach (var agent in agents)
{
var activeTickets = await _ticketRepository.GetCountAsync(
assigneeId: agent.Id,
status: TicketStatus.Open);
var inProgressTickets = await _ticketRepository.GetCountAsync(
assigneeId: agent.Id,
status: TicketStatus.InProgress);
agentWorkloads.Add(new AgentWorkloadDto
{
UserId = agent.Id,
UserName = $"{agent.FirstName} {agent.LastName}",
ActiveTickets = activeTickets + inProgressTickets,
ResolvedThisWeek = 0
});
}
return Ok(new DashboardStatsDto
{
TotalTickets = await _ticketRepository.GetTotalCountAsync(),
OpenTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Open),
InProgressTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.InProgress),
PendingTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Pending),
ResolvedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Resolved),
ClosedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Closed),
CriticalTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Critical),
HighTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.High),
MediumTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Medium),
LowTickets = await _ticketRepository.GetCountByPriorityAsync(TicketPriority.Low),
AverageResponseTimeMinutes = await _ticketRepository.GetAverageResponseTimeAsync(),
AverageResolutionTimeMinutes = await _ticketRepository.GetAverageResolutionTimeAsync(),
AgentWorkloads = agentWorkloads
});
}
}
@@ -0,0 +1,85 @@
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "Admin")]
public class GroupsController : ControllerBase
{
private readonly IGroupRepository _groupRepository;
public GroupsController(IGroupRepository groupRepository)
{
_groupRepository = groupRepository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<GroupDto>>> GetGroups()
{
var groups = await _groupRepository.GetAllAsync();
return Ok(groups.Select(MapToDto));
}
[HttpGet("{id}")]
public async Task<ActionResult<GroupDto>> GetGroup(Guid id)
{
var group = await _groupRepository.GetByIdAsync(id);
if (group == null) return NotFound(new { error = "Group not found." });
return Ok(MapToDto(group));
}
[HttpPost]
public async Task<ActionResult<GroupDto>> CreateGroup([FromBody] CreateGroupRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return BadRequest(new { error = "Name is required." });
var group = new Group
{
Name = request.Name,
Description = request.Description,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
var created = await _groupRepository.CreateAsync(group);
return CreatedAtAction(nameof(GetGroup), new { id = created.Id }, MapToDto(created));
}
[HttpPut("{id}")]
public async Task<ActionResult<GroupDto>> UpdateGroup(Guid id, [FromBody] CreateGroupRequest request)
{
var group = await _groupRepository.GetByIdAsync(id);
if (group == null) return NotFound(new { error = "Group not found." });
group.Name = request.Name;
if (request.Description != null) group.Description = request.Description;
var updated = await _groupRepository.UpdateAsync(group);
if (updated == null) return StatusCode(500, new { error = "Failed to update group." });
return Ok(MapToDto(updated));
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteGroup(Guid id)
{
var deleted = await _groupRepository.DeleteAsync(id);
if (!deleted) return NotFound(new { error = "Group not found." });
return NoContent();
}
private static GroupDto MapToDto(Group g) => new()
{
Id = g.Id,
Name = g.Name,
Description = g.Description,
IsActive = g.IsActive,
CreatedAt = g.CreatedAt
};
}
@@ -0,0 +1,206 @@
using System.Security.Claims;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/kb")]
[Authorize]
public class KnowledgeBaseController : ControllerBase
{
private readonly IKnowledgeBaseRepository _kbRepository;
public KnowledgeBaseController(IKnowledgeBaseRepository kbRepository)
{
_kbRepository = kbRepository;
}
// ===== Articles =====
[HttpGet("articles")]
[Authorize(Policy = "Agent")]
public async Task<ActionResult<IEnumerable<ArticleDto>>> GetAllArticles()
{
var articles = await _kbRepository.GetAllArticlesAsync();
return Ok(articles.Select(MapArticleToDto));
}
[HttpGet("articles/published")]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<ArticleDto>>> GetPublishedArticles()
{
var articles = await _kbRepository.GetPublishedArticlesAsync();
return Ok(articles.Select(MapArticleToDto));
}
[HttpGet("search")]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<ArticleDto>>> SearchArticles([FromQuery] string q)
{
if (string.IsNullOrWhiteSpace(q))
return BadRequest(new { error = "Search query is required." });
var articles = await _kbRepository.SearchArticlesAsync(q);
return Ok(articles.Select(MapArticleToDto));
}
[HttpGet("articles/{id}")]
public async Task<ActionResult<ArticleDto>> GetArticle(Guid id)
{
var article = await _kbRepository.GetArticleByIdAsync(id);
if (article == null) return NotFound(new { error = "Article not found." });
return Ok(MapArticleToDto(article));
}
[HttpPost("articles")]
[Authorize(Policy = "Agent")]
public async Task<ActionResult<ArticleDto>> CreateArticle([FromBody] CreateArticleRequest request)
{
if (string.IsNullOrWhiteSpace(request.Title))
return BadRequest(new { error = "Title is required." });
if (string.IsNullOrWhiteSpace(request.Content))
return BadRequest(new { error = "Content is required." });
var userId = GetUserId();
if (userId == null) return Unauthorized();
var article = new KnowledgeBaseArticle
{
Title = request.Title,
Content = request.Content,
CategoryId = request.CategoryId,
IsPublished = request.IsPublished,
AuthorId = userId.Value,
CreatedAt = DateTime.UtcNow
};
var created = await _kbRepository.CreateArticleAsync(article);
var full = await _kbRepository.GetArticleByIdAsync(created.Id);
return CreatedAtAction(nameof(GetArticle), new { id = created.Id }, MapArticleToDto(full!));
}
[HttpPut("articles/{id}")]
[Authorize(Policy = "Agent")]
public async Task<ActionResult<ArticleDto>> UpdateArticle(Guid id, [FromBody] UpdateArticleRequest request)
{
var article = await _kbRepository.GetArticleByIdAsync(id);
if (article == null) return NotFound(new { error = "Article not found." });
if (request.Title != null) article.Title = request.Title;
if (request.Content != null) article.Content = request.Content;
if (request.CategoryId.HasValue) article.CategoryId = request.CategoryId.Value;
if (request.IsPublished.HasValue) article.IsPublished = request.IsPublished.Value;
var updated = await _kbRepository.UpdateArticleAsync(article);
if (updated == null) return StatusCode(500, new { error = "Failed to update article." });
var full = await _kbRepository.GetArticleByIdAsync(id);
return Ok(MapArticleToDto(full!));
}
[HttpDelete("articles/{id}")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteArticle(Guid id)
{
var deleted = await _kbRepository.DeleteArticleAsync(id);
if (!deleted) return NotFound(new { error = "Article not found." });
return NoContent();
}
// ===== Categories =====
[HttpGet("categories")]
public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategories()
{
var categories = await _kbRepository.GetAllCategoriesAsync();
return Ok(categories.Select(MapCategoryToDto));
}
[HttpGet("categories/{id}")]
public async Task<ActionResult<CategoryDto>> GetCategory(Guid id)
{
var category = await _kbRepository.GetCategoryByIdAsync(id);
if (category == null) return NotFound(new { error = "Category not found." });
return Ok(MapCategoryToDto(category));
}
[HttpPost("categories")]
[Authorize(Policy = "Admin")]
public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CreateCategoryRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return BadRequest(new { error = "Name is required." });
var category = new KnowledgeBaseCategory
{
Name = request.Name,
Description = request.Description,
SortOrder = request.SortOrder,
CreatedAt = DateTime.UtcNow
};
var created = await _kbRepository.CreateCategoryAsync(category);
return CreatedAtAction(nameof(GetCategory), new { id = created.Id }, MapCategoryToDto(created));
}
[HttpPut("categories/{id}")]
[Authorize(Policy = "Admin")]
public async Task<ActionResult<CategoryDto>> UpdateCategory(Guid id, [FromBody] CreateCategoryRequest request)
{
var category = await _kbRepository.GetCategoryByIdAsync(id);
if (category == null) return NotFound(new { error = "Category not found." });
category.Name = request.Name;
category.Description = request.Description;
category.SortOrder = request.SortOrder;
var updated = await _kbRepository.UpdateCategoryAsync(category);
if (updated == null) return StatusCode(500, new { error = "Failed to update category." });
return Ok(MapCategoryToDto(updated));
}
[HttpDelete("categories/{id}")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteCategory(Guid id)
{
var deleted = await _kbRepository.DeleteCategoryAsync(id);
if (!deleted) return NotFound(new { error = "Category not found." });
return NoContent();
}
private Guid? GetUserId()
{
var claim = User.FindFirst("userId") ?? User.FindFirst(ClaimTypes.NameIdentifier);
return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null;
}
private static ArticleDto MapArticleToDto(KnowledgeBaseArticle a) => new()
{
Id = a.Id,
Title = a.Title,
Content = a.Content,
IsPublished = a.IsPublished,
CategoryId = a.CategoryId,
CategoryName = a.Category?.Name ?? "Unknown",
AuthorId = a.AuthorId,
AuthorName = a.Author != null ? $"{a.Author.FirstName} {a.Author.LastName}" : "Unknown",
ViewCount = a.ViewCount,
CreatedAt = a.CreatedAt,
UpdatedAt = a.UpdatedAt
};
private static CategoryDto MapCategoryToDto(KnowledgeBaseCategory c) => new()
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
SortOrder = c.SortOrder,
ArticleCount = c.Articles?.Count ?? 0,
CreatedAt = c.CreatedAt
};
}
@@ -0,0 +1,94 @@
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "Admin")]
public class SLAController : ControllerBase
{
private readonly ISLARepository _slaRepository;
public SLAController(ISLARepository slaRepository)
{
_slaRepository = slaRepository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<SLADto>>> GetSLAPolicies()
{
var policies = await _slaRepository.GetAllAsync();
return Ok(policies.Select(MapToDto));
}
[HttpGet("{id}")]
public async Task<ActionResult<SLADto>> GetSLAPolicy(Guid id)
{
var policy = await _slaRepository.GetByIdAsync(id);
if (policy == null) return NotFound(new { error = "SLA policy not found." });
return Ok(MapToDto(policy));
}
[HttpPost]
public async Task<ActionResult<SLADto>> CreateSLAPolicy([FromBody] CreateSLARequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return BadRequest(new { error = "Name is required." });
if (request.ResponseTimeHours <= 0 || request.ResolutionTimeHours <= 0)
return BadRequest(new { error = "Response and resolution times must be positive." });
var policy = new SLAPolicy
{
Name = request.Name,
Description = request.Description,
ResponseTimeHours = request.ResponseTimeHours,
ResolutionTimeHours = request.ResolutionTimeHours,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
var created = await _slaRepository.CreateAsync(policy);
return CreatedAtAction(nameof(GetSLAPolicy), new { id = created.Id }, MapToDto(created));
}
[HttpPut("{id}")]
public async Task<ActionResult<SLADto>> UpdateSLAPolicy(Guid id, [FromBody] UpdateSLARequest request)
{
var policy = await _slaRepository.GetByIdAsync(id);
if (policy == null) return NotFound(new { error = "SLA policy not found." });
if (request.Name != null) policy.Name = request.Name;
if (request.Description != null) policy.Description = request.Description;
if (request.ResponseTimeHours.HasValue) policy.ResponseTimeHours = request.ResponseTimeHours.Value;
if (request.ResolutionTimeHours.HasValue) policy.ResolutionTimeHours = request.ResolutionTimeHours.Value;
if (request.IsActive.HasValue) policy.IsActive = request.IsActive.Value;
var updated = await _slaRepository.UpdateAsync(policy);
if (updated == null) return StatusCode(500, new { error = "Failed to update SLA policy." });
return Ok(MapToDto(updated));
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteSLAPolicy(Guid id)
{
var deleted = await _slaRepository.DeleteAsync(id);
if (!deleted) return NotFound(new { error = "SLA policy not found." });
return NoContent();
}
private static SLADto MapToDto(SLAPolicy s) => new()
{
Id = s.Id,
Name = s.Name,
Description = s.Description,
ResponseTimeHours = s.ResponseTimeHours,
ResolutionTimeHours = s.ResolutionTimeHours,
IsActive = s.IsActive,
CreatedAt = s.CreatedAt
};
}
@@ -0,0 +1,204 @@
using System.Security.Claims;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using HelpDesk.Models.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TicketsController : ControllerBase
{
private readonly ITicketRepository _ticketRepository;
private readonly IUserRepository _userRepository;
public TicketsController(ITicketRepository ticketRepository, IUserRepository userRepository)
{
_ticketRepository = ticketRepository;
_userRepository = userRepository;
}
[HttpGet]
public async Task<ActionResult<TicketListResponse>> GetTickets(
[FromQuery] TicketStatus? status,
[FromQuery] TicketPriority? priority,
[FromQuery] Guid? assigneeId,
[FromQuery] Guid? requesterId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var userId = GetUserId();
if (userId == null) return Unauthorized();
var user = await _userRepository.GetByIdAsync(userId.Value);
if (user == null) return Unauthorized();
if (user.Role == UserRole.Customer)
requesterId = userId.Value;
var tickets = await _ticketRepository.GetAllAsync(status, priority, assigneeId, requesterId, page, pageSize);
var totalCount = await _ticketRepository.GetCountAsync(status, priority, assigneeId, requesterId);
return Ok(new TicketListResponse
{
Items = tickets.Select(MapToDto).ToList(),
TotalCount = totalCount,
Page = page,
PageSize = pageSize
});
}
[HttpGet("stats")]
public async Task<ActionResult<TicketStatsDto>> GetStats()
{
return Ok(new TicketStatsDto
{
TotalTickets = await _ticketRepository.GetTotalCountAsync(),
OpenTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Open),
InProgressTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.InProgress),
PendingTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Pending),
ResolvedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Resolved),
ClosedTickets = await _ticketRepository.GetCountByStatusAsync(TicketStatus.Closed)
});
}
[HttpGet("{id}")]
public async Task<ActionResult<TicketDto>> GetTicket(Guid id)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
var userId = GetUserId();
if (userId == null) return Unauthorized();
var user = await _userRepository.GetByIdAsync(userId.Value);
if (user?.Role == UserRole.Customer && ticket.RequesterId != userId.Value)
return Forbid();
return Ok(MapToDto(ticket));
}
[HttpPost]
public async Task<ActionResult<TicketDto>> CreateTicket([FromBody] CreateTicketRequest request)
{
if (string.IsNullOrWhiteSpace(request.Title))
return BadRequest(new { error = "Title is required." });
if (string.IsNullOrWhiteSpace(request.Description))
return BadRequest(new { error = "Description is required." });
var userId = GetUserId();
if (userId == null) return Unauthorized();
var ticket = new Ticket
{
Title = request.Title,
Description = request.Description,
Priority = request.Priority,
RequesterId = userId.Value,
GroupId = request.GroupId,
SlaPolicyId = request.SlaPolicyId,
TicketNumber = $"TK-{DateTime.UtcNow:yyyyMMddHHmmss}",
Status = TicketStatus.Open,
CreatedAt = DateTime.UtcNow
};
var created = await _ticketRepository.CreateAsync(ticket);
var full = await _ticketRepository.GetByIdAsync(created.Id);
return CreatedAtAction(nameof(GetTicket), new { id = created.Id }, MapToDto(full!));
}
[HttpPut("{id}")]
public async Task<ActionResult<TicketDto>> UpdateTicket(Guid id, [FromBody] UpdateTicketRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
if (!string.IsNullOrWhiteSpace(request.Title)) ticket.Title = request.Title;
if (!string.IsNullOrWhiteSpace(request.Description)) ticket.Description = request.Description;
if (request.Priority.HasValue) ticket.Priority = request.Priority.Value;
var updated = await _ticketRepository.UpdateAsync(ticket);
if (updated == null) return StatusCode(500, new { error = "Failed to update ticket." });
var full = await _ticketRepository.GetByIdAsync(id);
return Ok(MapToDto(full!));
}
[HttpDelete("{id}")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteTicket(Guid id)
{
var deleted = await _ticketRepository.DeleteAsync(id);
if (!deleted) return NotFound(new { error = "Ticket not found." });
return NoContent();
}
[HttpPut("{id}/status")]
[Authorize(Policy = "Agent")]
public async Task<IActionResult> UpdateStatus(Guid id, [FromBody] UpdateStatusRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
ticket.Status = request.Status;
await _ticketRepository.UpdateAsync(ticket);
return Ok(new { message = "Status updated.", status = request.Status.ToString() });
}
[HttpPut("{id}/assign")]
[Authorize(Policy = "Agent")]
public async Task<IActionResult> AssignTicket(Guid id, [FromBody] AssignTicketRequest request)
{
var ticket = await _ticketRepository.GetByIdAsync(id);
if (ticket == null) return NotFound(new { error = "Ticket not found." });
if (request.AssigneeId.HasValue)
{
var assignee = await _userRepository.GetByIdAsync(request.AssigneeId.Value);
if (assignee == null) return BadRequest(new { error = "Assignee not found." });
if (assignee.Role != UserRole.Agent && assignee.Role != UserRole.Admin)
return BadRequest(new { error = "Assignee must be an agent or admin." });
}
ticket.AssigneeId = request.AssigneeId;
if (request.AssigneeId.HasValue && ticket.Status == TicketStatus.Open)
ticket.Status = TicketStatus.InProgress;
await _ticketRepository.UpdateAsync(ticket);
return Ok(new { message = "Ticket assigned.", assigneeId = request.AssigneeId });
}
private Guid? GetUserId()
{
var claim = User.FindFirst("userId") ?? User.FindFirst(ClaimTypes.NameIdentifier);
return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null;
}
private static TicketDto MapToDto(Ticket t) => new()
{
Id = t.Id,
Title = t.Title,
Description = t.Description,
Status = t.Status,
Priority = t.Priority,
TicketNumber = t.TicketNumber,
RequesterId = t.RequesterId,
RequesterName = t.Requester != null ? $"{t.Requester.FirstName} {t.Requester.LastName}" : "Unknown",
AssigneeId = t.AssigneeId,
AssigneeName = t.Assignee != null ? $"{t.Assignee.FirstName} {t.Assignee.LastName}" : null,
GroupId = t.GroupId,
GroupName = t.Group?.Name,
SlaPolicyId = t.SlaPolicyId,
SlaPolicyName = t.SlaPolicy?.Name,
CreatedAt = t.CreatedAt,
UpdatedAt = t.UpdatedAt,
ResolvedAt = t.ResolvedAt,
CommentCount = t.Comments?.Count ?? 0
};
}
@@ -0,0 +1,99 @@
using System.Security.Claims;
using HelpDesk.Data.Repositories;
using HelpDesk.Models.DTOs;
using HelpDesk.Models.Entities;
using HelpDesk.Models.Enums;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelpDesk.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly IUserRepository _userRepository;
public UsersController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
[HttpGet]
[Authorize(Policy = "Admin")]
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers()
{
var users = await _userRepository.GetAllAsync();
return Ok(users.Select(MapToDto));
}
[HttpGet("agents")]
public async Task<ActionResult<IEnumerable<UserDto>>> GetAgents()
{
var agents = await _userRepository.GetByRoleAsync(UserRole.Agent);
return Ok(agents.Select(MapToDto));
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user == null) return NotFound(new { error = "User not found." });
return Ok(MapToDto(user));
}
[HttpPut("{id}")]
public async Task<ActionResult<UserDto>> UpdateUser(Guid id, [FromBody] UpdateUserRequest request)
{
var currentUserId = GetUserId();
if (currentUserId == null) return Unauthorized();
var currentUser = await _userRepository.GetByIdAsync(currentUserId.Value);
if (currentUser == null) return Unauthorized();
// Users can update themselves; Admins/SuperAdmins can update others
if (currentUserId.Value != id && currentUser.Role < UserRole.Admin)
return Forbid();
var user = await _userRepository.GetByIdAsync(id);
if (user == null) return NotFound(new { error = "User not found." });
if (request.FirstName != null) user.FirstName = request.FirstName;
if (request.LastName != null) user.LastName = request.LastName;
if (request.Email != null) user.Email = request.Email;
if (request.PhoneNumber != null) user.PhoneNumber = request.PhoneNumber;
if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
// Only admins can change role and active status
if (currentUser.Role >= UserRole.Admin)
{
if (request.Role.HasValue) user.Role = request.Role.Value;
if (request.IsActive.HasValue) user.IsActive = request.IsActive.Value;
}
var updated = await _userRepository.UpdateAsync(user);
if (updated == null) return StatusCode(500, new { error = "Failed to update user." });
return Ok(MapToDto(updated));
}
private Guid? GetUserId()
{
var claim = User.FindFirst("userId") ?? User.FindFirst(ClaimTypes.NameIdentifier);
return claim != null && Guid.TryParse(claim.Value, out var id) ? id : null;
}
private static UserDto MapToDto(User u) => new()
{
Id = u.Id,
Email = u.Email,
FirstName = u.FirstName,
LastName = u.LastName,
Role = u.Role,
IsActive = u.IsActive,
CreatedAt = u.CreatedAt,
PhoneNumber = u.PhoneNumber,
AvatarUrl = u.AvatarUrl
};
}