- 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>
302 lines
12 KiB
C#
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);
|
|
}
|
|
}
|