DeutschLernen/GermanApp/Application/Services/AdminService.cs
Lasse Rune Hansen 50f1b8a8dc feat(backend): Implement mandatory authentication and admin module
- 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>
2026-06-14 12:42:15 +02:00

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);
}
}