328 lines
11 KiB
C#
328 lines
11 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using System.Text.Json;
|
|
using DysonNetwork.Shared.Proto;
|
|
using DysonNetwork.Sphere.Publisher;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NodaTime;
|
|
|
|
namespace DysonNetwork.Sphere.Poll;
|
|
|
|
[ApiController]
|
|
[Route("/api/polls")]
|
|
public class PollController(AppDatabase db, PollService polls, Publisher.PublisherService pub) : ControllerBase
|
|
{
|
|
[HttpGet("{id:guid}")]
|
|
public async Task<ActionResult<PollWithStats>> GetPoll(Guid id)
|
|
{
|
|
var poll = await db.Polls
|
|
.Include(p => p.Questions)
|
|
.FirstOrDefaultAsync(p => p.Id == id);
|
|
if (poll is null) return NotFound("Poll not found");
|
|
var pollWithAnswer = PollWithStats.FromPoll(poll);
|
|
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Ok(pollWithAnswer);
|
|
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
var answer = await polls.GetPollAnswer(id, accountId);
|
|
if (answer is not null)
|
|
pollWithAnswer.UserAnswer = answer;
|
|
pollWithAnswer.Stats = await polls.GetPollStats(id);
|
|
|
|
return Ok(pollWithAnswer);
|
|
}
|
|
|
|
public class PollAnswerRequest
|
|
{
|
|
public required Dictionary<string, JsonElement> Answer { get; set; }
|
|
}
|
|
|
|
[HttpPost("{id:guid}/answer")]
|
|
[Authorize]
|
|
public async Task<ActionResult<PollAnswer>> AnswerPoll(Guid id, [FromBody] PollAnswerRequest request)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
try
|
|
{
|
|
return await polls.AnswerPoll(id, accountId, request.Answer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
[HttpDelete("{id:guid}/answer")]
|
|
[Authorize]
|
|
public async Task<IActionResult> DeletePollAnswer(Guid id)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
try
|
|
{
|
|
await polls.UnAnswerPoll(id, accountId);
|
|
return NoContent();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
[HttpGet("{id:guid}/feedback")]
|
|
public async Task<ActionResult<List<PollAnswer>>> GetPollFeedback(
|
|
Guid id,
|
|
[FromQuery] int offset = 0,
|
|
[FromQuery] int take = 20
|
|
)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
|
|
var poll = await db.Polls
|
|
.FirstOrDefaultAsync(p => p.Id == id);
|
|
if (poll is null) return NotFound("Poll not found");
|
|
|
|
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Viewer))
|
|
return StatusCode(403, "You need to be a viewer to view this poll's feedback.");
|
|
|
|
var answerQuery = db.PollAnswers
|
|
.Where(a => a.PollId == id)
|
|
.AsQueryable();
|
|
|
|
var total = await answerQuery.CountAsync();
|
|
Response.Headers.Append("X-Total", total.ToString());
|
|
|
|
var answers = await answerQuery
|
|
.OrderByDescending(a => a.CreatedAt)
|
|
.Skip(offset)
|
|
.Take(take)
|
|
.ToListAsync();
|
|
|
|
return Ok(answers);
|
|
}
|
|
|
|
[HttpGet("me")]
|
|
[Authorize]
|
|
public async Task<ActionResult<List<Poll>>> ListPolls(
|
|
[FromQuery(Name = "pub")] string? pubName,
|
|
[FromQuery] bool active = false,
|
|
[FromQuery] int offset = 0,
|
|
[FromQuery] int take = 20
|
|
)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
|
|
List<Guid> publishers;
|
|
if (pubName is null) publishers = (await pub.GetUserPublishers(accountId)).Select(p => p.Id).ToList();
|
|
else
|
|
{
|
|
publishers = await db.PublisherMembers
|
|
.Include(p => p.Publisher)
|
|
.Where(p => p.Publisher.Name == pubName && p.AccountId == accountId)
|
|
.Select(p => p.PublisherId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
var now = SystemClock.Instance.GetCurrentInstant();
|
|
var query = db.Polls
|
|
.Where(e => publishers.Contains(e.PublisherId));
|
|
if (active) query = query.Where(e => e.EndedAt > now);
|
|
|
|
var totalCount = await query.CountAsync();
|
|
HttpContext.Response.Headers.Append("X-Total", totalCount.ToString());
|
|
|
|
var polls = await query
|
|
.Skip(offset)
|
|
.Take(take)
|
|
.Include(p => p.Questions)
|
|
.ToListAsync();
|
|
return Ok(polls);
|
|
}
|
|
|
|
public class PollRequest
|
|
{
|
|
public string? Title { get; set; }
|
|
public string? Description { get; set; }
|
|
public Instant? EndedAt { get; set; }
|
|
public List<PollRequestQuestion>? Questions { get; set; }
|
|
}
|
|
|
|
public class PollRequestQuestion
|
|
{
|
|
public Guid Id { get; set; } = Guid.NewGuid();
|
|
|
|
public PollQuestionType Type { get; set; }
|
|
public List<PollOption>? Options { get; set; }
|
|
|
|
[MaxLength(1024)] public string Title { get; set; } = null!;
|
|
[MaxLength(4096)] public string? Description { get; set; }
|
|
public int Order { get; set; } = 0;
|
|
public bool IsRequired { get; set; }
|
|
|
|
public PollQuestion ToQuestion() => new()
|
|
{
|
|
Id = Id,
|
|
Type = Type,
|
|
Options = Options,
|
|
Title = Title,
|
|
Description = Description,
|
|
Order = Order,
|
|
IsRequired = IsRequired
|
|
};
|
|
}
|
|
|
|
[HttpPost]
|
|
[Authorize]
|
|
public async Task<ActionResult<Poll>> CreatePoll([FromBody] PollRequest request,
|
|
[FromQuery(Name = "pub")] string pubName)
|
|
{
|
|
if (request.Questions is null) return BadRequest("Questions are required.");
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
|
|
var publisher = await pub.GetPublisherByName(pubName);
|
|
if (publisher is null) return BadRequest("Publisher was not found.");
|
|
if (!await pub.IsMemberWithRole(publisher.Id, accountId, Publisher.PublisherMemberRole.Editor))
|
|
return StatusCode(403, "You need at least be an editor to create polls as this publisher.");
|
|
|
|
var poll = new Poll
|
|
{
|
|
Title = request.Title,
|
|
Description = request.Description,
|
|
EndedAt = request.EndedAt,
|
|
PublisherId = publisher.Id,
|
|
Questions = request.Questions.Select(q => q.ToQuestion()).ToList()
|
|
};
|
|
|
|
try
|
|
{
|
|
polls.ValidatePoll(poll);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return BadRequest(ex.Message);
|
|
}
|
|
|
|
db.Polls.Add(poll);
|
|
await db.SaveChangesAsync();
|
|
return Ok(poll);
|
|
}
|
|
|
|
[HttpPatch("{id:guid}")]
|
|
[Authorize]
|
|
public async Task<ActionResult<Poll>> UpdatePoll(Guid id, [FromBody] PollRequest request)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
|
|
// Start a transaction
|
|
await using var transaction = await db.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
var poll = await db.Polls
|
|
.Include(p => p.Questions)
|
|
.FirstOrDefaultAsync(p => p.Id == id);
|
|
|
|
if (poll == null) return NotFound("Poll not found");
|
|
|
|
// Check if user is an editor of the publisher that owns the poll
|
|
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
|
|
return StatusCode(403, "You need to be at least an editor to update this poll.");
|
|
|
|
// Update properties if they are provided in the request
|
|
if (request.Title != null) poll.Title = request.Title;
|
|
if (request.Description != null) poll.Description = request.Description;
|
|
if (request.EndedAt.HasValue) poll.EndedAt = request.EndedAt;
|
|
|
|
db.Update(poll);
|
|
await db.SaveChangesAsync();
|
|
|
|
// Update questions if provided
|
|
if (request.Questions != null)
|
|
{
|
|
await db.PollQuestions
|
|
.Where(p => p.PollId == poll.Id)
|
|
.ExecuteDeleteAsync();
|
|
var newQuestions = request.Questions
|
|
.Select(q => q.ToQuestion())
|
|
.Select(q =>
|
|
{
|
|
q.PollId = poll.Id;
|
|
return q;
|
|
})
|
|
.ToList();
|
|
db.PollQuestions.AddRange(newQuestions);
|
|
await db.SaveChangesAsync();
|
|
poll.Questions = newQuestions;
|
|
}
|
|
|
|
polls.ValidatePoll(poll);
|
|
|
|
// Commit the transaction if all operations succeed
|
|
await transaction.CommitAsync();
|
|
|
|
return Ok(poll);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
return BadRequest(ex.Message);
|
|
}
|
|
}
|
|
|
|
[HttpDelete("{id:guid}")]
|
|
[Authorize]
|
|
public async Task<IActionResult> DeletePoll(Guid id)
|
|
{
|
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
|
var accountId = Guid.Parse(currentUser.Id);
|
|
|
|
// Start a transaction
|
|
await using var transaction = await db.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
var poll = await db.Polls
|
|
.Include(p => p.Questions)
|
|
.FirstOrDefaultAsync(p => p.Id == id);
|
|
|
|
if (poll == null) return NotFound("Poll not found");
|
|
|
|
// Check if user is an editor of the publisher that owns the poll
|
|
if (!await pub.IsMemberWithRole(poll.PublisherId, accountId, Publisher.PublisherMemberRole.Editor))
|
|
return StatusCode(403, "You need to be at least an editor to delete this poll.");
|
|
|
|
// Delete all answers for this poll
|
|
var answers = await db.PollAnswers
|
|
.Where(a => a.PollId == id)
|
|
.ToListAsync();
|
|
|
|
if (answers.Count != 0)
|
|
db.PollAnswers.RemoveRange(answers);
|
|
|
|
// Delete all questions for this poll
|
|
if (poll.Questions.Count != 0)
|
|
db.PollQuestions.RemoveRange(poll.Questions);
|
|
|
|
// Finally, delete the poll itself
|
|
db.Polls.Remove(poll);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
// Commit the transaction if all operations succeed
|
|
await transaction.CommitAsync();
|
|
|
|
return NoContent();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
return StatusCode(500, "An error occurred while deleting the poll... " + ex.Message);
|
|
}
|
|
}
|
|
} |