- Add Role property to User entity with migration - Create BootstrapController for first admin user creation - Remove [AllowAnonymous] from all learning content controllers - Create AdminController with admin-only endpoints - Create AdminService for user management - Create UserReportService for progress reports - Add UserRepository implementation - Update AuthService with role support feat(frontend): Implement authentication system - Add AuthStore with React context for auth state management - Create Login, Register, Landing, and Home pages - Add ProtectedRoute and AdminRoute components - Create Auth API types and client - Configure Vite with @/ path alias - Add comprehensive CSS styles for auth and landing pages BREAKING CHANGE: All learning content now requires authentication. Users must register and sign in before accessing lessons, quizzes, and stories. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
269 lines
10 KiB
C#
269 lines
10 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.Net;
|
|
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
|
|
{
|
|
var adminUserId = int.Parse(User.FindFirst("nameid")?.Value ?? "0");
|
|
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);
|
|
}
|
|
}
|
|
}
|