using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Driver; using QuestionsApi.Models; using QuestionsApi.Services; namespace QuestionsApi.Controllers; [ApiController] [Route("api/[controller]")] public class QuestionsController : ControllerBase { private readonly QuestionStore _questions; public QuestionsController(QuestionStore questions) { _questions = questions; } [HttpGet("systems")] [Authorize(Roles = "USER,SUPER")] public async Task Systems() { var systems = await _questions.GetSystemsAsync(User.IsInRole("SUPER")); return Ok(systems); } [HttpPost("search")] [Authorize(Roles = "USER,SUPER")] public async Task Search([FromBody] QuestionSearchRequest req) { var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER")); return Ok(questions.Select(QuestionResponse.FromModel).ToList()); } [HttpGet("{id}")] [Authorize(Roles = "USER,SUPER")] public async Task Get(string id) { var question = await _questions.GetByIdAsync(id); if (question is null || (!question.IsPublished && !User.IsInRole("SUPER"))) return NotFound(); return Ok(QuestionResponse.FromModel(question)); } [HttpPost] [Authorize(Roles = "SUPER")] public async Task Create([FromBody] UpsertQuestionRequest req) { var validation = ValidateQuestion(req); if (validation is not null) return BadRequest(validation); var created = await _questions.CreateAsync(req); if (created is null) return Conflict("Question externalId already exists"); return Ok(QuestionResponse.FromModel(created)); } [HttpPost("bulk")] [Authorize(Roles = "SUPER")] public async Task BulkUpsert([FromBody] BulkUpsertQuestionsRequest req) { if (req.Questions.Count == 0) return BadRequest("questions required"); foreach (var question in req.Questions) { var validation = ValidateQuestion(question); if (validation is not null) return BadRequest($"{question.ExternalId}: {validation}"); } var upserted = await _questions.UpsertManyByExternalIdAsync(req.Questions); return Ok(new { upserted }); } [HttpPut("{id}")] [Authorize(Roles = "SUPER")] public async Task Update(string id, [FromBody] UpsertQuestionRequest req) { var validation = ValidateQuestion(req); if (validation is not null) return BadRequest(validation); try { var updated = await _questions.UpdateAsync(id, req); if (updated is null) return NotFound(); return Ok(QuestionResponse.FromModel(updated)); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return Conflict("Question externalId already exists"); } } [HttpDelete("{id}")] [Authorize(Roles = "SUPER")] public async Task Delete(string id) { var deleted = await _questions.DeleteAsync(id); if (!deleted) return NotFound(); return Ok("Deleted"); } private static string? ValidateQuestion(UpsertQuestionRequest req) { if (string.IsNullOrWhiteSpace(req.ExternalId)) return "externalId required"; if (string.IsNullOrWhiteSpace(req.Stem)) return "stem required"; if (string.IsNullOrWhiteSpace(req.LeadIn)) return "leadIn required"; if (req.Choices.Count < 2) return "at least two choices required"; if (string.IsNullOrWhiteSpace(req.CorrectChoiceId)) return "correctChoiceId required"; if (!req.Choices.Any(c => c.Id == req.CorrectChoiceId)) return "correctChoiceId must match a choice id"; if (string.IsNullOrWhiteSpace(req.Explanation)) return "explanation required"; if (string.IsNullOrWhiteSpace(req.System)) return "system required"; if (string.IsNullOrWhiteSpace(req.Discipline)) return "discipline required"; if (req.Difficulty is < 1 or > 5) return "difficulty must be between 1 and 5"; return null; } }