- 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>
196 lines
6.9 KiB
C#
196 lines
6.9 KiB
C#
using GermanApp.Application.DTOs;
|
|
using GermanApp.Application.Interfaces;
|
|
using GermanApp.Domain.Entities;
|
|
using GermanApp.Domain.Interfaces;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace GermanApp.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Service for admin operations.
|
|
/// Part of the Application layer.
|
|
/// </summary>
|
|
public class AdminService : IAdminService
|
|
{
|
|
private readonly IRepository<User, int> _userRepository;
|
|
private readonly ILevelRepository _levelRepository;
|
|
private readonly ILessonRepository _lessonRepository;
|
|
private readonly IQuizRepository _quizRepository;
|
|
private readonly IStoryRepository _storyRepository;
|
|
private readonly IUserProgressRepository _userProgressRepository;
|
|
|
|
public AdminService(
|
|
IRepository<User, int> userRepository,
|
|
ILevelRepository levelRepository,
|
|
ILessonRepository lessonRepository,
|
|
IQuizRepository quizRepository,
|
|
IStoryRepository storyRepository,
|
|
IUserProgressRepository userProgressRepository)
|
|
{
|
|
_userRepository = userRepository;
|
|
_levelRepository = levelRepository;
|
|
_lessonRepository = lessonRepository;
|
|
_quizRepository = quizRepository;
|
|
_storyRepository = storyRepository;
|
|
_userProgressRepository = userProgressRepository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all users for admin view.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of all users</returns>
|
|
public async Task<IReadOnlyList<AdminUserListItemDto>> GetAllUsersAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var users = await _userRepository.GetAllAsync(cancellationToken);
|
|
|
|
return users.Select(u => new AdminUserListItemDto(
|
|
u.Id,
|
|
u.Username,
|
|
u.Email,
|
|
u.Role,
|
|
u.CurrentLevel,
|
|
u.TotalPoints,
|
|
u.Streak,
|
|
u.CreatedAt))
|
|
.OrderBy(u => u.Username)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific user by ID.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The user DTO</returns>
|
|
public async Task<AdminUserDto?> GetUserByIdAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
|
|
|
|
if (user == null)
|
|
return null;
|
|
|
|
return new AdminUserDto(
|
|
user.Id,
|
|
user.Username,
|
|
user.Email,
|
|
user.Role,
|
|
user.CurrentLevel,
|
|
user.Streak,
|
|
user.TotalPoints,
|
|
user.CreatedAt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a user's role.
|
|
/// </summary>
|
|
/// <param name="userId">The user ID</param>
|
|
/// <param name="newRole">The new role</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The updated user DTO</returns>
|
|
public async Task<AdminUserDto?> UpdateUserRoleAsync(int userId, string newRole, CancellationToken cancellationToken = default)
|
|
{
|
|
var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
|
|
|
|
if (user == null)
|
|
return null;
|
|
|
|
// Validate role and update using domain method
|
|
user.SetRole(newRole);
|
|
|
|
await _userRepository.UpdateAsync(user, cancellationToken);
|
|
|
|
return new AdminUserDto(
|
|
user.Id,
|
|
user.Username,
|
|
user.Email,
|
|
user.Role,
|
|
user.CurrentLevel,
|
|
user.Streak,
|
|
user.TotalPoints,
|
|
user.CreatedAt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a user (admin only, cannot delete self).
|
|
/// </summary>
|
|
/// <param name="userId">The user ID to delete</param>
|
|
/// <param name="adminUserId">The admin user ID making the request</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>True if successful</returns>
|
|
public async Task<bool> DeleteUserAsync(int userId, int adminUserId, CancellationToken cancellationToken = default)
|
|
{
|
|
// Cannot delete self
|
|
if (userId == adminUserId)
|
|
throw new InvalidOperationException("Cannot delete your own user account.");
|
|
|
|
// Check if user exists
|
|
var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
|
|
if (user == null)
|
|
return false;
|
|
|
|
// Prevent deleting the last admin
|
|
var allUsers = await _userRepository.GetAllAsync(cancellationToken);
|
|
var adminCount = allUsers.Count(u => u.IsAdmin());
|
|
if (user.IsAdmin() && adminCount <= 1)
|
|
throw new InvalidOperationException("Cannot delete the last admin user.");
|
|
|
|
await _userRepository.DeleteAsync(user, cancellationToken);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets admin dashboard statistics.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Dashboard statistics</returns>
|
|
public async Task<AdminDashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var usersTask = _userRepository.GetAllAsync(cancellationToken);
|
|
var lessonsTask = _lessonRepository.GetAllAsync(cancellationToken);
|
|
var quizzesTask = _quizRepository.GetAllAsync(cancellationToken);
|
|
var userProgressTask = _userProgressRepository.GetAllAsync(cancellationToken);
|
|
|
|
await Task.WhenAll(usersTask, lessonsTask, quizzesTask, userProgressTask);
|
|
|
|
var users = await usersTask;
|
|
var lessons = await lessonsTask;
|
|
var quizzes = await quizzesTask;
|
|
var userProgress = await userProgressTask;
|
|
|
|
// Get all levels and count story segments
|
|
var levels = await _levelRepository.GetAllAsync(cancellationToken);
|
|
var storyCount = 0;
|
|
foreach (var level in levels)
|
|
{
|
|
var segments = await _storyRepository.GetByLevelAsync(level.Id, true, cancellationToken);
|
|
storyCount += segments.Count;
|
|
}
|
|
|
|
// Calculate active users this week
|
|
var oneWeekAgo = DateTime.UtcNow.AddDays(-7);
|
|
var activeUsersThisWeek = userProgress
|
|
.Where(up => up.LastAttemptDate >= oneWeekAgo)
|
|
.Select(up => up.UserId)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
// Calculate average user progress
|
|
var completedLessons = userProgress.Count(up => up.IsCompleted);
|
|
var totalPossibleProgress = users.Count * lessons.Count;
|
|
var averageProgress = totalPossibleProgress > 0
|
|
? (double)completedLessons / totalPossibleProgress * 100
|
|
: 0;
|
|
|
|
return new AdminDashboardStatsDto(
|
|
users.Count,
|
|
lessons.Count,
|
|
quizzes.Count,
|
|
storyCount,
|
|
activeUsersThisWeek,
|
|
averageProgress);
|
|
}
|
|
}
|