Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions api/Controllers/ReservationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace Controllers
{
[Tags("Reservations"), Route("reservation")]
public class ReservationController : Controller
public class ReservationController : StaffAccessController
{
private ReservationRepository _repo { get; set; }

Expand All @@ -18,14 +18,24 @@ public ReservationController(ReservationRepository reservationRepository)
[HttpGet, Produces("application/json"), Route("")]
public async Task<ActionResult<Reservation>> GetReservations()
{
var reservations = await _repo.GetReservations();
if (IsNotStaff(Request, out ActionResult? result))
{
return result!;
}

var reservations = await _repo.GetUpcomingReservations();

return Json(reservations);
Comment on lines 18 to 28

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetReservations is declared as Task<ActionResult<Reservation>> but it returns a collection (Json(reservations)). This can confuse Swagger / consumers and weakens type-safety. Update the signature to Task<ActionResult<IEnumerable<Reservation>>> (or similar) to accurately reflect the response shape.

Copilot uses AI. Check for mistakes.
}

[HttpGet, Produces("application/json"), Route("{reservationId}")]
public async Task<ActionResult<Reservation>> GetRoom(Guid reservationId)
{
if (IsNotStaff(Request, out ActionResult? result))
{
return result!;
}

try
{
var reservation = await _repo.GetReservation(reservationId);
Expand Down Expand Up @@ -56,7 +66,19 @@ [FromBody] Reservation newBooking
try
{
var createdReservation = await _repo.CreateReservation(newBooking);
return Created($"/reservation/${createdReservation.Id}", createdReservation);
return Created($"/reservation/{createdReservation.Id}", createdReservation);
}
catch (ReservationConflictException ex)
{
return Conflict(ex.Message);
}
catch (InvalidReservationException ex)
{
return BadRequest(ex.Message);
}
catch (NotFoundException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
Expand All @@ -74,5 +96,35 @@ public async Task<IActionResult> DeleteReservation(Guid reservationId)

return result ? NoContent() : NotFound();
}

[HttpPost, Produces("application/json"), Route("{reservationId}/check-in")]
public async Task<ActionResult<Reservation>> CheckInReservation(
Guid reservationId,
[FromBody] CheckInReservationRequest request
)
{
if (IsNotStaff(Request, out ActionResult? result))
{
return result!;
}

try
{
var checkedInReservation = await _repo.CheckInReservation(
reservationId,
request.GuestEmail
);

return Json(checkedInReservation);
}
catch (NotFoundException)
{
return NotFound();
}
catch (InvalidCheckInException ex)
{
return BadRequest(ex.Message);
}
}
}
}
27 changes: 27 additions & 0 deletions api/Controllers/StaffAccessController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;

namespace Controllers
{
public abstract class StaffAccessController : Controller
{
protected const string StaffAccessCookieName = "access";

/// <summary>
/// Checks if the request is from a staff member, if not returns true and a 403 result
/// </summary>
protected bool IsNotStaff(HttpRequest request, out ActionResult? result)
{
// TODO explore UseAuthentication
request.Cookies.TryGetValue(StaffAccessCookieName, out string? accessValue);

if (accessValue == null || accessValue == "0")
{
result = StatusCode(403);
return true;
}

result = null;
return false;
}
}
}
27 changes: 4 additions & 23 deletions api/Controllers/StaffController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Controllers
{
[Route("staff")]
public class StaffController : Controller
public class StaffController : StaffAccessController
{
private IConfiguration Config { get; set; }

Expand All @@ -12,25 +12,6 @@ public StaffController(IConfiguration config)
Config = config;
}

/// <summary>
/// Checks if the request is from a staff member, if not returns true and a 403 result
/// </summary>
/// <param name="request"></param>
private bool IsNotStaff(HttpRequest request, out IActionResult? result)
{
// TODO explore UseAuthentication
request.Cookies.TryGetValue("access", out string? accessValue);

if (accessValue == null || accessValue == "0")
{
result = StatusCode(403);
return true;
}

result = null;
return false;
}

[HttpGet, Route("login")]
public IActionResult CheckCode([FromHeader(Name = "X-Staff-Code")] string accessCode)
{
Expand All @@ -41,15 +22,15 @@ public IActionResult CheckCode([FromHeader(Name = "X-Staff-Code")] string access
return NoContent();
}
Response.Cookies.Append(
"access",
StaffAccessCookieName,
"1",
new CookieOptions
// TODO evaluate cookie options & auth mechanism for best security practices
{
IsEssential = true,
SameSite = SameSiteMode.Strict,
HttpOnly = true,
Secure = true
Secure = Request.IsHttps
}
);
return NoContent();
Expand All @@ -58,7 +39,7 @@ public IActionResult CheckCode([FromHeader(Name = "X-Staff-Code")] string access
[HttpGet, Route("check")]
public IActionResult CheckCookie()
{
if (IsNotStaff(Request, out IActionResult? result))
if (IsNotStaff(Request, out ActionResult? result))
{
return result!;
}
Expand Down
7 changes: 7 additions & 0 deletions api/Models/CheckInReservationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Models
{
public class CheckInReservationRequest
{
public required string GuestEmail { get; set; }
}
}
8 changes: 8 additions & 0 deletions api/Models/Errors/InvalidCheckInException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Models.Errors
{
public class InvalidCheckInException : Exception
{
public InvalidCheckInException(string message)
: base(message) { }
}
}
8 changes: 8 additions & 0 deletions api/Models/Errors/InvalidReservationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Models.Errors
{
public class InvalidReservationException : Exception
{
public InvalidReservationException(string message)
: base(message) { }
}
}
8 changes: 8 additions & 0 deletions api/Models/Errors/ReservationConflictException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Models.Errors
{
public class ReservationConflictException : Exception
{
public ReservationConflictException(string message)
: base(message) { }
}
}
20 changes: 20 additions & 0 deletions api/Models/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,28 @@ public static string FormatRoomNumber(int number)
return number.ToString().PadLeft(3, '0');
}

public static bool IsValidRoomNumber(string roomNumber)
{
if (roomNumber.Length != 3)
{
return false;
}

if (!roomNumber.All(char.IsAsciiDigit))
{
return false;
}

return roomNumber[1] != '0' || roomNumber[2] != '0';
}

public static int ConvertRoomNumberToInt(string roomNumber)
{
if (!IsValidRoomNumber(roomNumber))
{
throw new InvalidRoomNumber(roomNumber);
}

var success = int.TryParse(roomNumber, out int roomNumberInt);
if (!success)
{
Expand Down
Loading
Loading