profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
InvitationsController.cs 6,755 bytes
1 using System.Security.Claims;
2 using System.Security.Cryptography;
3 using Asp.Versioning;
4 using MediatR;
5 using Microsoft.AspNetCore.Authentication.JwtBearer;
6 using Microsoft.AspNetCore.Authorization;
7 using Microsoft.AspNetCore.Mvc;
8 using SplitApp.Modules.Trips.Api.Dto.v1;
9 using SplitApp.Modules.Trips.Application.Contracts;
10 using SplitApp.Modules.Trips.Domain.Entities;
11 using SplitApp.Modules.Trips.Domain.Enums;
12 using SplitApp.Shared.Contracts.Users.Queries;
13
14 namespace SplitApp.Modules.Trips.Api.Controllers;
15
16 [ApiVersion("1.0")]
17 [ApiController]
18 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
19 [Route("api/v{version:apiVersion}/[controller]")]
20 public class InvitationsController : ControllerBase
21 {
22 private readonly ITripsUnitOfWork _uow;
23 private readonly IMediator _mediator;
24
25 public InvitationsController(ITripsUnitOfWork uow, IMediator mediator)
26 {
27 _uow = uow;
28 _mediator = mediator;
29 }
30
31 [HttpPost]
32 public async Task<ActionResult<InvitationDto>> Create([FromBody] InvitationCreateDto dto)
33 {
34 var userId = CurrentUserId();
35 if (userId == null) return Unauthorized();
36
37 if (!await IsOrganizerAsync(dto.TripId, userId.Value)) return Forbid();
38
39 var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
40 .Replace("+", "-").Replace("/", "_").TrimEnd('=');
41
42 var invitation = new TripInvitation
43 {
44 TripId = dto.TripId,
45 InvitedByUserId = userId.Value,
46 Token = token,
47 Status = EInvitationStatus.Pending,
48 ExpiresAt = DateTime.UtcNow.AddDays(7),
49 };
50
51 _uow.Invitations.Add(invitation);
52 await _uow.SaveChangesAsync();
53
54 return CreatedAtAction(nameof(GetByToken), new { token = invitation.Token }, await BuildDtoAsync(invitation));
55 }
56
57 [HttpGet("{token}")]
58 [AllowAnonymous]
59 public async Task<ActionResult<InvitationDto>> GetByToken(string token)
60 {
61 var invitation = await FindByTokenAsync(token);
62 if (invitation == null) return NotFound();
63
64 return Ok(await BuildDtoAsync(invitation));
65 }
66
67 [HttpPost("{token}/accept")]
68 public async Task<IActionResult> Accept(string token)
69 {
70 var userId = CurrentUserId();
71 if (userId == null) return Unauthorized();
72
73 var invitation = await FindByTokenAsync(token);
74 if (invitation == null) return NotFound();
75
76 if (invitation.Status != EInvitationStatus.Pending)
77 return BadRequest("Invitation is no longer pending.");
78
79 if (invitation.ExpiresAt < DateTime.UtcNow)
80 {
81 invitation.Status = EInvitationStatus.Expired;
82 _uow.Invitations.Update(invitation);
83 await _uow.SaveChangesAsync();
84 return BadRequest("Invitation has expired.");
85 }
86
87 var allParticipants = await _uow.Participants.GetAllAsync();
88 var existingActive = allParticipants.FirstOrDefault(p =>
89 p.TripId == invitation.TripId && p.UserId == userId.Value && p.IsActive);
90 var existingInactive = allParticipants.FirstOrDefault(p =>
91 p.TripId == invitation.TripId && p.UserId == userId.Value && !p.IsActive);
92
93 if (existingActive != null)
94 {
95 return BadRequest("You are already a participant in this trip.");
96 }
97
98 if (existingInactive != null)
99 {
100 existingInactive.IsActive = true;
101 existingInactive.LeftAt = null;
102 _uow.Participants.Update(existingInactive);
103 }
104 else
105 {
106 _uow.Participants.Add(new TripParticipant
107 {
108 TripId = invitation.TripId,
109 UserId = userId.Value,
110 Role = EParticipantRole.Participant,
111 JoinedAt = DateTime.UtcNow,
112 IsActive = true,
113 });
114 }
115
116 invitation.Status = EInvitationStatus.Accepted;
117 invitation.RespondedAt = DateTime.UtcNow;
118 _uow.Invitations.Update(invitation);
119
120 await _uow.SaveChangesAsync();
121 return Ok();
122 }
123
124 [HttpPost("{token}/decline")]
125 public async Task<IActionResult> Decline(string token)
126 {
127 var invitation = await FindByTokenAsync(token);
128 if (invitation == null) return NotFound();
129
130 if (invitation.Status != EInvitationStatus.Pending)
131 return BadRequest("Invitation is no longer pending.");
132
133 invitation.Status = EInvitationStatus.Declined;
134 invitation.RespondedAt = DateTime.UtcNow;
135 _uow.Invitations.Update(invitation);
136 await _uow.SaveChangesAsync();
137 return Ok();
138 }
139
140 [HttpPost("{token}/revoke")]
141 public async Task<IActionResult> Revoke(string token)
142 {
143 var userId = CurrentUserId();
144 if (userId == null) return Unauthorized();
145
146 var invitation = await FindByTokenAsync(token);
147 if (invitation == null) return NotFound();
148
149 if (!await IsOrganizerAsync(invitation.TripId, userId.Value)) return Forbid();
150
151 invitation.Status = EInvitationStatus.Revoked;
152 invitation.RespondedAt = DateTime.UtcNow;
153 _uow.Invitations.Update(invitation);
154 await _uow.SaveChangesAsync();
155 return Ok();
156 }
157
158 private Guid? CurrentUserId()
159 {
160 var raw = User.FindFirstValue(ClaimTypes.NameIdentifier);
161 return Guid.TryParse(raw, out var id) ? id : null;
162 }
163
164 private async Task<TripInvitation?> FindByTokenAsync(string token)
165 {
166 var all = await _uow.Invitations.GetAllAsync();
167 return all.FirstOrDefault(i => i.Token == token);
168 }
169
170 private async Task<bool> IsOrganizerAsync(Guid tripId, Guid userId)
171 {
172 var trip = await _uow.Trips.GetByIdAsync(tripId);
173 if (trip == null) return false;
174 if (trip.CreatedById == userId) return true;
175 var all = await _uow.Participants.GetAllAsync();
176 return all.Any(p => p.TripId == tripId
177 && p.UserId == userId
178 && p.IsActive
179 && p.Role == EParticipantRole.Organizer);
180 }
181
182 private async Task<InvitationDto> BuildDtoAsync(TripInvitation invitation)
183 {
184 var trip = await _uow.Trips.GetByIdAsync(invitation.TripId);
185 var inviter = await _mediator.Send(new GetUserByIdQuery(invitation.InvitedByUserId));
186
187 return new InvitationDto
188 {
189 Id = invitation.Id,
190 TripId = invitation.TripId,
191 TripName = trip?.Name,
192 Token = invitation.Token,
193 Status = invitation.Status.ToString(),
194 ExpiresAt = invitation.ExpiresAt,
195 InvitedByUserName = inviter?.DisplayName,
196 };
197 }
198 }
199