profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
PollsController.cs 5,921 bytes
1 using App.BLL.DTO;
2 using App.BLL.Services;
3 using App.Domain;
4 using App.DTO.Mappers;
5 using App.DTO.v1;
6 using Asp.Versioning;
7 using Microsoft.AspNetCore.Authentication.JwtBearer;
8 using Microsoft.AspNetCore.Authorization;
9 using Microsoft.AspNetCore.Mvc;
10 using System.Net;
11 using System.Security.Claims;
12
13 namespace WebApp.ApiControllers;
14
15 [ApiVersion("1.0")]
16 [Route("api/v{version:apiVersion}/[controller]")]
17 [ApiController]
18 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
19 public class PollsController : ControllerBase
20 {
21 private readonly IPollService _pollService;
22 private readonly ITripService _tripService;
23
24 public PollsController(IPollService pollService, ITripService tripService)
25 {
26 _pollService = pollService;
27 _tripService = tripService;
28 }
29
30 private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
31
32 public class VoteRequest
33 {
34 public Guid OptionId { get; set; }
35 }
36
37 // GET: api/v1/polls/trip/{tripId}
38 [HttpGet("trip/{tripId:guid}")]
39 [Produces("application/json")]
40 [ProducesResponseType<List<PollDto>>((int)HttpStatusCode.OK)]
41 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
42 public async Task<ActionResult<List<PollDto>>> GetTripPolls(Guid tripId)
43 {
44 var userId = GetUserId();
45
46 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
47
48 var polls = await _pollService.GetByTripIdAsync(tripId, userId);
49
50 return Ok(polls.Select(p => PollMapper.MapToDto(p, userId)).ToList());
51 }
52
53 // POST: api/v1/polls
54 [HttpPost]
55 [Produces("application/json")]
56 [Consumes("application/json")]
57 [ProducesResponseType<PollDto>((int)HttpStatusCode.Created)]
58 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
59 public async Task<ActionResult<PollDto>> CreatePoll([FromBody] PollCreateDto dto)
60 {
61 var userId = GetUserId();
62
63 var poll = new TripPollBllDto
64 {
65 TripId = dto.TripId,
66 CreatedByUserId = userId,
67 Question = dto.Question,
68 AllowMultipleVotes = dto.AllowMultipleVotes,
69 IsAnonymous = dto.IsAnonymous
70 };
71
72 var (created, errorCode) = await _pollService.CreatePollGuardedAsync(poll, dto.Options, userId);
73 if (created == null)
74 {
75 if (errorCode == "forbidden") return Forbid();
76 return NotFound();
77 }
78
79 // Reload with navigation properties
80 var reloaded = await _pollService.GetByIdWithDetailsAsync(created.Id, userId);
81
82 return CreatedAtAction(nameof(GetPoll), new { id = reloaded!.Id }, PollMapper.MapToDto(reloaded, userId));
83 }
84
85 // GET: api/v1/polls/{id}
86 [HttpGet("{id:guid}")]
87 [Produces("application/json")]
88 [ProducesResponseType<PollDto>((int)HttpStatusCode.OK)]
89 [ProducesResponseType((int)HttpStatusCode.NotFound)]
90 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
91 public async Task<ActionResult<PollDto>> GetPoll(Guid id)
92 {
93 var userId = GetUserId();
94
95 var poll = await _pollService.GetByIdWithDetailsAsync(id, userId);
96 if (poll == null)
97 {
98 // distinguish not-found vs forbidden
99 // No helper exposes a raw getter through service; attempt via GetByIdAsync(id, Guid.Empty) returns null for both,
100 // so fall back to NotFound (treat both as NotFound is acceptable for API IDOR-hardening).
101 return NotFound();
102 }
103
104 return Ok(PollMapper.MapToDto(poll, userId));
105 }
106
107 // POST: api/v1/polls/{id}/vote
108 [HttpPost("{id:guid}/vote")]
109 [Consumes("application/json")]
110 [Produces("application/json")]
111 [ProducesResponseType((int)HttpStatusCode.OK)]
112 [ProducesResponseType((int)HttpStatusCode.NotFound)]
113 [ProducesResponseType((int)HttpStatusCode.BadRequest)]
114 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
115 public async Task<IActionResult> CastVote(Guid id, [FromBody] VoteRequest request)
116 {
117 var userId = GetUserId();
118
119 var (ok, errorCode) = await _pollService.CastVoteAsync(id, request.OptionId, userId);
120 if (!ok)
121 {
122 return errorCode switch
123 {
124 "notfound" => NotFound(),
125 "closed" => BadRequest("Poll is closed."),
126 "forbidden" => Forbid(),
127 "invalid-option" => BadRequest("Invalid option."),
128 _ => NotFound()
129 };
130 }
131
132 return Ok();
133 }
134
135 // POST: api/v1/polls/{id}/close
136 [HttpPost("{id:guid}/close")]
137 [Produces("application/json")]
138 [ProducesResponseType((int)HttpStatusCode.OK)]
139 [ProducesResponseType((int)HttpStatusCode.NotFound)]
140 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
141 public async Task<IActionResult> ClosePoll(Guid id)
142 {
143 var userId = GetUserId();
144
145 var (ok, errorCode) = await _pollService.ClosePollAsync(id, userId, organizerAllowed: true);
146 if (!ok)
147 {
148 return errorCode switch
149 {
150 "notfound" => NotFound(),
151 "forbidden" => Forbid(),
152 _ => NotFound()
153 };
154 }
155
156 return Ok();
157 }
158
159 // DELETE: api/v1/polls/{id}
160 [HttpDelete("{id:guid}")]
161 [ProducesResponseType((int)HttpStatusCode.NoContent)]
162 [ProducesResponseType((int)HttpStatusCode.NotFound)]
163 [ProducesResponseType((int)HttpStatusCode.Forbidden)]
164 public async Task<IActionResult> DeletePoll(Guid id)
165 {
166 var userId = GetUserId();
167
168 var (ok, errorCode) = await _pollService.DeletePollGuardedAsync(id, userId);
169 if (!ok)
170 {
171 return errorCode switch
172 {
173 "notfound" => NotFound(),
174 "forbidden" => Forbid(),
175 _ => NotFound()
176 };
177 }
178
179 return NoContent();
180 }
181 }
182