profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

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