- Changed AuthService.GenerateJwtToken to use JWT standard claims: - JwtRegisteredClaimNames.Sub for user ID (instead of ClaimTypes.NameIdentifier) - JwtRegisteredClaimNames.Name for username - JwtRegisteredClaimNames.Email for email - JwtRegisteredClaimNames.UniqueName for additional username claim - Kept ClaimTypes.Role for role - Updated AuthController.GetCurrentUser to use JwtRegisteredClaimNames.Sub - Updated AdminController.DeleteUserAsync to use JwtRegisteredClaimNames.Sub This fixes the 401 error when calling /me after login. The issue was that tokens were being generated with ClaimTypes.NameIdentifier claim, but the JWT middleware doesn't automatically map this to a claim that can be found with User.FindFirst(). Using standard JWT claims (sub, name, email) ensures proper compatibility with ASP.NET Core's JWT authentication. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
275 lines
11 KiB
C#
275 lines
11 KiB
C#
using GermanApp.Application.DTOs;
|
|
using GermanApp.Application.Interfaces;
|
|
using GermanApp.Application.DTOs.Auth;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System.Collections.Generic;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Net;
|
|
using System.Security.Claims;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace GermanApp.Presentation.Controllers;
|
|
|
|
/// <summary>
|
|
/// API controller for admin operations.
|
|
/// All endpoints require Admin role.
|
|
/// This is part of the Presentation layer.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize(Roles = "Admin")]
|
|
public class AdminController : ControllerBase
|
|
{
|
|
private readonly IAdminService _adminService;
|
|
private readonly IUserReportService _reportService;
|
|
|
|
public AdminController(IAdminService adminService, IUserReportService reportService)
|
|
{
|
|
_adminService = adminService;
|
|
_reportService = reportService;
|
|
}
|
|
|
|
// ============================================
|
|
// USER MANAGEMENT ENDPOINTS
|
|
// ============================================
|
|
|
|
/// <summary>
|
|
/// Gets all users (Admin only).
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of all users</returns>
|
|
[HttpGet("users")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<AdminUserListItemDto>), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetAllUsersAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var users = await _adminService.GetAllUsersAsync(cancellationToken);
|
|
return Ok(users);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific user by ID (Admin only).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The user DTO</returns>
|
|
[HttpGet("users/{userId}")]
|
|
[ProducesResponseType(typeof(AdminUserDto), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetUserByIdAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _adminService.GetUserByIdAsync(userId, cancellationToken);
|
|
|
|
if (user == null)
|
|
return NotFound();
|
|
|
|
return Ok(user);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a user's role (Admin only).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="dto">The role update data</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The updated user DTO</returns>
|
|
[HttpPut("users/{userId}/role")]
|
|
[ProducesResponseType(typeof(AdminUserDto), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> UpdateUserRoleAsync(
|
|
int userId,
|
|
[FromBody] UpdateUserRoleDto dto,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var user = await _adminService.UpdateUserRoleAsync(userId, dto.Role, cancellationToken);
|
|
|
|
if (user == null)
|
|
return NotFound();
|
|
|
|
return Ok(user);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a user (Admin only, cannot delete self).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID to delete</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Success or error</returns>
|
|
[HttpDelete("users/{userId}")]
|
|
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> DeleteUserAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
// Get admin user ID from JWT "sub" claim (standard JWT claim for subject/user ID)
|
|
var adminUserIdClaim = User.FindFirst(JwtRegisteredClaimNames.Sub) ?? User.FindFirst("sub");
|
|
if (adminUserIdClaim == null || !int.TryParse(adminUserIdClaim.Value, out var adminUserId) || adminUserId == 0)
|
|
return Unauthorized();
|
|
|
|
var result = await _adminService.DeleteUserAsync(userId, adminUserId, cancellationToken);
|
|
|
|
if (!result)
|
|
return NotFound();
|
|
|
|
return NoContent();
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// DASHBOARD ENDPOINTS
|
|
// ============================================
|
|
|
|
/// <summary>
|
|
/// Gets admin dashboard statistics.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Dashboard statistics</returns>
|
|
[HttpGet("dashboard/stats")]
|
|
[ProducesResponseType(typeof(AdminDashboardStatsDto), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetDashboardStatsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var stats = await _adminService.GetDashboardStatsAsync(cancellationToken);
|
|
return Ok(stats);
|
|
}
|
|
|
|
// ============================================
|
|
// REPORT ENDPOINTS
|
|
// ============================================
|
|
|
|
/// <summary>
|
|
/// Generates a progress report for a specific user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>User progress report</returns>
|
|
[HttpGet("reports/users/{userId}")]
|
|
[ProducesResponseType(typeof(UserProgressReportDto), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetUserReportAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var report = await _reportService.GenerateUserReportAsync(userId, cancellationToken);
|
|
return Ok(report);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates progress reports for all users.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of all user progress reports</returns>
|
|
[HttpGet("reports/all-users")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<UserProgressReportDto>), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetAllUserReportsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var reports = await _reportService.GenerateAllUserReportsAsync(cancellationToken);
|
|
return Ok(reports);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets lesson completion data for a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of lesson completion data</returns>
|
|
[HttpGet("reports/users/{userId}/lessons")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<LessonCompletionDto>), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetUserLessonProgressAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
var progress = await _reportService.GetUserLessonProgressAsync(userId, cancellationToken);
|
|
return Ok(progress);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets quiz results for a user.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of quiz results</returns>
|
|
[HttpGet("reports/users/{userId}/quizzes")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<UserQuizResultDto>), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetUserQuizResultsAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
var results = await _reportService.GetUserQuizResultsAsync(userId, cancellationToken);
|
|
return Ok(results);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exports all user reports as CSV.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>CSV content</returns>
|
|
[HttpGet("reports/export/csv")]
|
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> ExportReportsAsCsvAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var csv = await _reportService.ExportReportsAsCsvAsync(cancellationToken);
|
|
return Ok(new { CsvContent = csv });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific user's full report (combined data).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Combined user report data</returns>
|
|
[HttpGet("reports/users/{userId}/full")]
|
|
[ProducesResponseType((int)HttpStatusCode.OK)]
|
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
public async Task<IActionResult> GetUserFullReportAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var progressReportTask = _reportService.GenerateUserReportAsync(userId, cancellationToken);
|
|
var lessonProgressTask = _reportService.GetUserLessonProgressAsync(userId, cancellationToken);
|
|
var quizResultsTask = _reportService.GetUserQuizResultsAsync(userId, cancellationToken);
|
|
|
|
await Task.WhenAll(progressReportTask, lessonProgressTask, quizResultsTask);
|
|
|
|
return Ok(new
|
|
{
|
|
ProgressReport = await progressReportTask,
|
|
LessonProgress = await lessonProgressTask,
|
|
QuizResults = await quizResultsTask
|
|
});
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
}
|