using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using QuestionsApi.Models; using QuestionsApi.Services; using System.Security.Claims; namespace QuestionsApi.Controllers; [ApiController] [Route("api/question-reports")] public class QuestionReportsController : ControllerBase { private const int MinDetailsLength = 10; private const int MaxDetailsLength = 2000; private const int MaxReviewerNoteLength = 2000; private readonly QuestionStore _questions; private readonly QuestionReportStore _reports; public QuestionReportsController(QuestionStore questions, QuestionReportStore reports) { _questions = questions; _reports = reports; } [HttpPost] [Authorize(Roles = "USER,SUPER")] public async Task Create([FromBody] CreateQuestionReportRequest request) { var questionExternalId = (request.QuestionExternalId ?? "").Trim(); var category = (request.Category ?? "").Trim().ToLowerInvariant(); var details = (request.Details ?? "").Trim(); if (string.IsNullOrWhiteSpace(questionExternalId)) return BadRequest("questionExternalId required"); if (!QuestionReportCategory.All.Contains(category)) return BadRequest("category is invalid"); if (details.Length < MinDetailsLength || details.Length > MaxDetailsLength) return BadRequest($"details must be between {MinDetailsLength} and {MaxDetailsLength} characters"); var question = await _questions.GetByExternalIdAsync(questionExternalId); if (question is null || (!question.IsPublished && !User.IsInRole("SUPER"))) return NotFound(); var userId = CurrentUserId(); if (string.IsNullOrWhiteSpace(userId)) return Unauthorized(); var report = await _reports.CreateAsync(question, userId, category, details); return StatusCode(StatusCodes.Status201Created, new CreateQuestionReportResponse { Id = report.Id ?? "", QuestionExternalId = report.QuestionExternalId, Category = report.Category, Status = report.Status, CreatedUtc = report.CreatedUtc }); } [HttpGet] [Authorize(Roles = "SUPER")] public async Task List( [FromQuery] string status = QuestionReportStatus.Open, [FromQuery] string category = "", [FromQuery] string questionExternalId = "", [FromQuery] string userId = "", [FromQuery] int skip = 0, [FromQuery] int limit = 50) { status = status.Trim().ToLowerInvariant(); category = category.Trim().ToLowerInvariant(); questionExternalId = questionExternalId.Trim(); userId = userId.Trim(); if (status != "all" && status != QuestionReportStatus.Open && !QuestionReportStatus.Terminal.Contains(status)) return BadRequest("status is invalid"); if (!string.IsNullOrWhiteSpace(category) && !QuestionReportCategory.All.Contains(category)) return BadRequest("category is invalid"); if (skip < 0) return BadRequest("skip must be zero or greater"); if (limit is < 1 or > 100) return BadRequest("limit must be between 1 and 100"); return Ok(await _reports.SearchAsync(status, category, questionExternalId, userId, skip, limit)); } [HttpGet("{id}")] [Authorize(Roles = "SUPER")] public async Task Get(string id) { var report = await _reports.GetByIdAsync(id.Trim()); return report is null ? NotFound() : Ok(report); } [HttpPatch("{id}/status")] [Authorize(Roles = "SUPER")] public async Task UpdateStatus(string id, [FromBody] UpdateQuestionReportStatusRequest request) { var status = (request.Status ?? "").Trim().ToLowerInvariant(); var reviewerNote = (request.ReviewerNote ?? "").Trim(); if (!QuestionReportStatus.Terminal.Contains(status)) return BadRequest("status must be resolved or dismissed"); if (reviewerNote.Length > MaxReviewerNoteLength) return BadRequest($"reviewerNote must be {MaxReviewerNoteLength} characters or fewer"); var existing = await _reports.GetByIdAsync(id.Trim()); if (existing is null) return NotFound(); if (existing.Status != QuestionReportStatus.Open) return Conflict("report is already closed"); var reviewerUserId = CurrentUserId(); if (string.IsNullOrWhiteSpace(reviewerUserId)) return Unauthorized(); var updated = await _reports.CloseAsync(id.Trim(), status, reviewerUserId, reviewerNote); return updated is null ? Conflict("report is already closed") : Ok(updated); } private string CurrentUserId() => User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name) ?? User.Identity?.Name ?? ""; }