DeutschLernen/GermanApp/Presentation/Controllers/QuizQuestionsController.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

302 lines
12 KiB
C#

using GermanApp.Application.DTOs;
using GermanApp.Application.Services;
using GermanApp.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace GermanApp.Presentation.Controllers;
/// <summary>
/// API controller for managing quiz questions.
/// This is part of the Presentation layer.
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class QuizQuestionsController : ControllerBase
{
private readonly QuizQuestionService _quizQuestionService;
public QuizQuestionsController(QuizQuestionService quizQuestionService)
{
_quizQuestionService = quizQuestionService;
}
/// <summary>
/// Gets all quiz questions.
/// </summary>
/// <returns>List of all quiz questions</returns>
[HttpGet]
public async Task<IActionResult> GetAllQuizQuestionsAsync(CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetAllQuizQuestionsAsync(cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets all active quiz questions for a specific quiz.
/// </summary>
/// <param name="quizId">The quiz ID</param>
/// <returns>List of quiz questions for the specified quiz</returns>
[HttpGet("by-quiz/{quizId}")]
public async Task<IActionResult> GetQuizQuestionsByQuizAsync(int quizId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetActiveQuizQuestionsByQuizAsync(quizId, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets all active quiz questions for a specific lesson (legacy - uses first quiz).
/// </summary>
/// <param name="lessonId">The lesson ID</param>
/// <returns>List of quiz questions for the specified lesson</returns>
[HttpGet("by-lesson/{lessonId}")]
public async Task<IActionResult> GetQuizQuestionsByLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetActiveQuizQuestionsByLessonAsync(lessonId, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets a specific quiz question by its ID.
/// </summary>
/// <param name="id">The quiz question ID</param>
/// <returns>The quiz question with the specified ID</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetQuizQuestionByIdAsync(int id, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionService.GetQuizQuestionByIdAsync(id, cancellationToken);
if (question == null)
return NotFound();
return Ok(question);
}
/// <summary>
/// Gets a random set of quiz questions for a quiz.
/// </summary>
/// <param name="quizId">The quiz ID</param>
/// <param name="count">Number of questions to return</param>
/// <returns>List of random quiz questions for the quiz</returns>
[HttpGet("random/{quizId}/{count}")]
public async Task<IActionResult> GetRandomQuizQuestionsForQuizAsync(int quizId, int count, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetRandomQuizQuestionsForQuizAsync(quizId, count, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets a random set of quiz questions for a lesson (legacy - uses first quiz).
/// </summary>
/// <param name="lessonId">The lesson ID</param>
/// <param name="count">Number of questions to return</param>
/// <returns>List of random quiz questions for the lesson</returns>
[HttpGet("random/by-lesson/{lessonId}/{count}")]
public async Task<IActionResult> GetRandomQuizQuestionsForLessonAsync(int lessonId, int count, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetRandomQuizQuestionsForLessonAsync(lessonId, count, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets quiz questions by difficulty level.
/// </summary>
/// <param name="difficulty">The difficulty level (1-5)</param>
/// <returns>List of quiz questions with the specified difficulty</returns>
[HttpGet("by-difficulty/{difficulty}")]
public async Task<IActionResult> GetQuizQuestionsByDifficultyAsync(int difficulty, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetQuizQuestionsByDifficultyAsync(difficulty, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Gets quiz questions by type.
/// </summary>
/// <param name="type">The question type</param>
/// <returns>List of quiz questions with the specified type</returns>
[HttpGet("by-type/{type}")]
public async Task<IActionResult> GetQuizQuestionsByTypeAsync(QuestionType type, CancellationToken cancellationToken = default)
{
var questions = await _quizQuestionService.GetQuizQuestionsByTypeAsync(type, cancellationToken);
return Ok(questions);
}
/// <summary>
/// Creates a new quiz question.
/// </summary>
/// <param name="dto">The quiz question data (requires QuizId)</param>
/// <returns>The created quiz question</returns>
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateQuizQuestionAsync(CreateQuizQuestionDto dto, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionService.CreateQuizQuestionAsync(dto, cancellationToken);
return CreatedAtAction(nameof(GetQuizQuestionByIdAsync), new { id = question.Id }, question);
}
/// <summary>
/// Updates an existing quiz question.
/// </summary>
/// <param name="id">The quiz question ID</param>
/// <param name="dto">The updated quiz question data</param>
/// <returns>The updated quiz question</returns>
[HttpPut("{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateQuizQuestionAsync(int id, UpdateQuizQuestionDto dto, CancellationToken cancellationToken = default)
{
var question = await _quizQuestionService.UpdateQuizQuestionAsync(id, dto, cancellationToken);
if (question == null)
return NotFound();
return Ok(question);
}
/// <summary>
/// Deletes a quiz question by its ID.
/// </summary>
/// <param name="id">The quiz question ID</param>
/// <returns>No content on success</returns>
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteQuizQuestionAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _quizQuestionService.DeleteQuizQuestionAsync(id, cancellationToken);
if (!result)
return NotFound();
return NoContent();
}
/// <summary>
/// Gets quiz options for a specific question.
/// </summary>
/// <param name="questionId">The question ID</param>
/// <returns>List of quiz options for the question</returns>
[HttpGet("options/{questionId}")]
public async Task<IActionResult> GetQuizOptionsByQuestionAsync(int questionId, CancellationToken cancellationToken = default)
{
var options = await _quizQuestionService.GetQuizOptionsByQuestionAsync(questionId, cancellationToken);
return Ok(options);
}
/// <summary>
/// Gets the correct option for a question.
/// </summary>
/// <param name="questionId">The question ID</param>
/// <returns>The correct option for the question</returns>
[HttpGet("correct-option/{questionId}")]
public async Task<IActionResult> GetCorrectOptionAsync(int questionId, CancellationToken cancellationToken = default)
{
var option = await _quizQuestionService.GetCorrectOptionAsync(questionId, cancellationToken);
if (option == null)
return NotFound();
return Ok(option);
}
/// <summary>
/// Gets all correct options for a question (for MultipleSelect type).
/// </summary>
/// <param name="questionId">The question ID</param>
/// <returns>List of correct options for the question</returns>
[HttpGet("correct-options/{questionId}")]
public async Task<IActionResult> GetCorrectOptionsAsync(int questionId, CancellationToken cancellationToken = default)
{
var options = await _quizQuestionService.GetCorrectOptionsAsync(questionId, cancellationToken);
return Ok(options);
}
/// <summary>
/// Creates a new quiz option for a question.
/// </summary>
/// <param name="questionId">The question ID</param>
/// <param name="dto">The quiz option data</param>
/// <returns>The created quiz option</returns>
[HttpPost("options/{questionId}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> CreateQuizOptionAsync(int questionId, CreateQuizOptionDto dto, CancellationToken cancellationToken = default)
{
var option = await _quizQuestionService.CreateQuizOptionAsync(questionId, dto, cancellationToken);
return CreatedAtAction(nameof(GetQuizOptionsByQuestionAsync), new { questionId }, option);
}
/// <summary>
/// Updates an existing quiz option.
/// </summary>
/// <param name="id">The quiz option ID</param>
/// <param name="dto">The updated quiz option data</param>
/// <returns>The updated quiz option</returns>
[HttpPut("options/{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> UpdateQuizOptionAsync(int id, UpdateQuizOptionDto dto, CancellationToken cancellationToken = default)
{
var option = await _quizQuestionService.UpdateQuizOptionAsync(id, dto, cancellationToken);
if (option == null)
return NotFound();
return Ok(option);
}
/// <summary>
/// Deletes a quiz option by its ID.
/// </summary>
/// <param name="id">The quiz option ID</param>
/// <returns>No content on success</returns>
[HttpDelete("options/{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteQuizOptionAsync(int id, CancellationToken cancellationToken = default)
{
var result = await _quizQuestionService.DeleteQuizOptionAsync(id, cancellationToken);
if (!result)
return NotFound();
return NoContent();
}
/// <summary>
/// Submits quiz answers and returns the result.
/// </summary>
/// <param name="userId">The user ID</param>
/// <param name="dto">The quiz answers submission (requires QuizId)</param>
/// <returns>The quiz result</returns>
[HttpPost("submit/{userId}")]
[Authorize]
public async Task<IActionResult> SubmitQuizAnswersAsync(int userId, SubmitQuizAnswersDto dto, CancellationToken cancellationToken = default)
{
// Convert legacy DTO to new format
var newDto = new SubmitQuizDto(dto.QuizId, dto.Answers);
var result = await _quizQuestionService.SubmitQuizAnswersForQuizAsync(userId, newDto, cancellationToken);
return Ok(result);
}
/// <summary>
/// Gets the number of quiz questions for a lesson.
/// </summary>
/// <param name="lessonId">The lesson ID</param>
/// <returns>The count of quiz questions for the lesson</returns>
[HttpGet("count/{lessonId}")]
public async Task<IActionResult> GetQuizQuestionCountForLessonAsync(int lessonId, CancellationToken cancellationToken = default)
{
var count = await _quizQuestionService.GetQuizQuestionCountForLessonAsync(lessonId, cancellationToken);
return Ok(new { lessonId, count });
}
/// <summary>
/// Gets the difficulty distribution for a lesson.
/// </summary>
/// <param name="lessonId">The lesson ID</param>
/// <returns>The difficulty distribution for the lesson</returns>
[HttpGet("difficulty-distribution/{lessonId}")]
public async Task<IActionResult> GetDifficultyDistributionAsync(int lessonId, CancellationToken cancellationToken = default)
{
var distribution = await _quizQuestionService.GetDifficultyDistributionAsync(lessonId, cancellationToken);
return Ok(distribution);
}
/// <summary>
/// Gets the type distribution for a lesson.
/// </summary>
/// <param name="lessonId">The lesson ID</param>
/// <returns>The type distribution for the lesson</returns>
[HttpGet("type-distribution/{lessonId}")]
public async Task<IActionResult> GetTypeDistributionAsync(int lessonId, CancellationToken cancellationToken = default)
{
var distribution = await _quizQuestionService.GetTypeDistributionAsync(lessonId, cancellationToken);
return Ok(distribution);
}
}