mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Enforce brewery ownership on update and delete (#267)
This commit is contained in:
@@ -32,6 +32,9 @@ public class CreateBreweryHandlerTests
|
||||
.Setup(r => r.CreateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Returns(Task.CompletedTask);
|
||||
_repoMock
|
||||
.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() => persisted);
|
||||
|
||||
DateTime before = DateTime.UtcNow;
|
||||
BreweryDto result = await _handler.Handle(command, CancellationToken.None);
|
||||
@@ -53,8 +56,27 @@ public class CreateBreweryHandlerTests
|
||||
persisted.Location.PostalCode.Should().Be(command.Location.PostalCode);
|
||||
|
||||
_repoMock.Verify(r => r.CreateAsync(It.IsAny<BreweryPost>()), Times.Once);
|
||||
_repoMock.Verify(
|
||||
r => r.GetByIdAsync(persisted.BreweryPostId, It.IsAny<CancellationToken>()),
|
||||
Times.Once
|
||||
);
|
||||
|
||||
result.BreweryName.Should().Be("MyBrew");
|
||||
result.Location.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ThrowsInvalidOperationException_WhenBreweryNotFoundAfterCreation()
|
||||
{
|
||||
CreateBreweryCommand command = new(Guid.NewGuid(), "MyBrew", "Desc", ValidLocation());
|
||||
|
||||
_repoMock.Setup(r => r.CreateAsync(It.IsAny<BreweryPost>())).Returns(Task.CompletedTask);
|
||||
_repoMock
|
||||
.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((BreweryPost?)null);
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,66 @@
|
||||
using Domain.Exceptions;
|
||||
using Features.Breweries.Commands.DeleteBrewery;
|
||||
using Features.Breweries.Repository;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace Features.Breweries.Tests.Commands;
|
||||
|
||||
public class DeleteBreweryHandlerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Handle_DelegatesToRepository()
|
||||
private readonly DeleteBreweryHandler _handler;
|
||||
private readonly Mock<IBreweryRepository> _repoMock = new();
|
||||
|
||||
public DeleteBreweryHandlerTests()
|
||||
{
|
||||
Mock<IBreweryRepository> repoMock = new();
|
||||
DeleteBreweryHandler handler = new(repoMock.Object);
|
||||
Guid id = Guid.NewGuid();
|
||||
repoMock.Setup(r => r.DeleteAsync(id)).Returns(Task.CompletedTask);
|
||||
_handler = new DeleteBreweryHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
await handler.Handle(new DeleteBreweryCommand(id), CancellationToken.None);
|
||||
private void SetUpExistingBrewery(Guid breweryPostId, Guid postedById)
|
||||
{
|
||||
_repoMock
|
||||
.Setup(r => r.GetPostedByIdAsync(breweryPostId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(postedById);
|
||||
}
|
||||
|
||||
repoMock.Verify(r => r.DeleteAsync(id), Times.Once);
|
||||
[Fact]
|
||||
public async Task Handle_DelegatesToRepository_WhenRequestingUserIsThePoster()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
Guid requestingUserId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, requestingUserId);
|
||||
_repoMock.Setup(r => r.DeleteAsync(breweryPostId)).Returns(Task.CompletedTask);
|
||||
|
||||
await _handler.Handle(
|
||||
new DeleteBreweryCommand(breweryPostId, requestingUserId),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
_repoMock.Verify(r => r.DeleteAsync(breweryPostId), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ThrowsNotFoundException_WhenBreweryDoesNotExist()
|
||||
{
|
||||
DeleteBreweryCommand command = new(Guid.NewGuid(), Guid.NewGuid());
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
_repoMock.Verify(r => r.DeleteAsync(It.IsAny<Guid>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ThrowsForbiddenException_WhenRequestingUserIsNotThePoster()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, Guid.NewGuid());
|
||||
|
||||
DeleteBreweryCommand command = new(breweryPostId, Guid.NewGuid());
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ForbiddenException>();
|
||||
_repoMock.Verify(r => r.DeleteAsync(It.IsAny<Guid>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
using Features.Breweries.Commands.UpdateBrewery;
|
||||
using Features.Breweries.Repository;
|
||||
using FluentAssertions;
|
||||
@@ -16,13 +17,24 @@ public class UpdateBreweryHandlerTests
|
||||
_handler = new UpdateBreweryHandler(_repoMock.Object);
|
||||
}
|
||||
|
||||
private void SetUpExistingBrewery(Guid breweryPostId, Guid postedById)
|
||||
{
|
||||
_repoMock
|
||||
.Setup(r => r.GetPostedByIdAsync(breweryPostId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(postedById);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UpdatesNameDescription_AndSetsUpdatedAt()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
Guid requestingUserId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, requestingUserId);
|
||||
|
||||
UpdateBreweryCommand command = new(
|
||||
Guid.NewGuid(),
|
||||
breweryPostId,
|
||||
requestingUserId,
|
||||
[0x01, 0x02],
|
||||
Guid.NewGuid(),
|
||||
"Renamed",
|
||||
"New description",
|
||||
null
|
||||
@@ -30,8 +42,8 @@ public class UpdateBreweryHandlerTests
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<BreweryPost, CancellationToken>((b, _) => persisted = b)
|
||||
.ReturnsAsync(() => persisted!);
|
||||
|
||||
DateTime before = DateTime.UtcNow;
|
||||
@@ -40,7 +52,6 @@ public class UpdateBreweryHandlerTests
|
||||
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.BreweryPostId.Should().Be(command.BreweryPostId);
|
||||
persisted.PostedById.Should().Be(command.PostedById);
|
||||
persisted.BreweryName.Should().Be("Renamed");
|
||||
persisted.Description.Should().Be("New description");
|
||||
persisted.RowVersion.Should().Equal(command.RowVersion);
|
||||
@@ -51,10 +62,14 @@ public class UpdateBreweryHandlerTests
|
||||
[Fact]
|
||||
public async Task Handle_ClearsLocation_WhenCommandLocationIsNull()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
Guid requestingUserId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, requestingUserId);
|
||||
|
||||
UpdateBreweryCommand command = new(
|
||||
Guid.NewGuid(),
|
||||
breweryPostId,
|
||||
requestingUserId,
|
||||
[0x01, 0x02],
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
"Description",
|
||||
null
|
||||
@@ -62,8 +77,8 @@ public class UpdateBreweryHandlerTests
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<BreweryPost, CancellationToken>((b, _) => persisted = b)
|
||||
.ReturnsAsync(() => persisted!);
|
||||
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
@@ -74,6 +89,10 @@ public class UpdateBreweryHandlerTests
|
||||
[Fact]
|
||||
public async Task Handle_SetsLocation_WhenCommandLocationProvided()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
Guid requestingUserId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, requestingUserId);
|
||||
|
||||
UpdateBreweryLocation locationCommand = new(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
@@ -83,9 +102,9 @@ public class UpdateBreweryHandlerTests
|
||||
null
|
||||
);
|
||||
UpdateBreweryCommand command = new(
|
||||
Guid.NewGuid(),
|
||||
breweryPostId,
|
||||
requestingUserId,
|
||||
[0x01, 0x02],
|
||||
Guid.NewGuid(),
|
||||
"Name",
|
||||
"Description",
|
||||
locationCommand
|
||||
@@ -93,8 +112,8 @@ public class UpdateBreweryHandlerTests
|
||||
|
||||
BreweryPost? persisted = null;
|
||||
_repoMock
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>()))
|
||||
.Callback<BreweryPost>(b => persisted = b)
|
||||
.Setup(r => r.UpdateAsync(It.IsAny<BreweryPost>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<BreweryPost, CancellationToken>((b, _) => persisted = b)
|
||||
.ReturnsAsync(() => persisted!);
|
||||
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
@@ -109,4 +128,41 @@ public class UpdateBreweryHandlerTests
|
||||
persisted.Location.AddressLine2.Should().Be(locationCommand.AddressLine2);
|
||||
persisted.Location.PostalCode.Should().Be(locationCommand.PostalCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ThrowsNotFoundException_WhenBreweryDoesNotExist()
|
||||
{
|
||||
UpdateBreweryCommand command = new(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
[0x01, 0x02],
|
||||
"Name",
|
||||
"Description",
|
||||
null
|
||||
);
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<NotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ThrowsForbiddenException_WhenRequestingUserIsNotThePoster()
|
||||
{
|
||||
Guid breweryPostId = Guid.NewGuid();
|
||||
SetUpExistingBrewery(breweryPostId, Guid.NewGuid());
|
||||
|
||||
UpdateBreweryCommand command = new(
|
||||
breweryPostId,
|
||||
Guid.NewGuid(),
|
||||
[0x01, 0x02],
|
||||
"Name",
|
||||
"Description",
|
||||
null
|
||||
);
|
||||
|
||||
Func<Task> act = () => _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<ForbiddenException>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class GetAllBreweryLocationsHandlerTests
|
||||
{
|
||||
_repoMock.Setup(r => r.GetAllLocations()).ReturnsAsync(Array.Empty<BreweryPost>());
|
||||
|
||||
IEnumerable<SimplifiedBreweryDto> result = await _handler.Handle(
|
||||
IEnumerable<BreweryWithLocationDto> result = await _handler.Handle(
|
||||
new GetAllBreweryLocationsQuery(),
|
||||
CancellationToken.None
|
||||
);
|
||||
@@ -41,7 +41,7 @@ public class GetAllBreweryLocationsHandlerTests
|
||||
];
|
||||
_repoMock.Setup(r => r.GetAllLocations()).ReturnsAsync(breweries);
|
||||
|
||||
IEnumerable<SimplifiedBreweryDto> result = await _handler.Handle(
|
||||
IEnumerable<BreweryWithLocationDto> result = await _handler.Handle(
|
||||
new GetAllBreweryLocationsQuery(),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ public class GetBreweryLocationsWithinRangeHandlerTests
|
||||
)
|
||||
.ReturnsAsync(Array.Empty<BreweryPost>());
|
||||
|
||||
IEnumerable<SimplifiedBreweryDto> result = await _handler.Handle(
|
||||
IEnumerable<BreweryWithLocationDto> result = await _handler.Handle(
|
||||
new GetBreweryLocationsWithinRangeQuery(43.6532, -79.3832, 1000),
|
||||
CancellationToken.None
|
||||
);
|
||||
@@ -57,7 +57,7 @@ public class GetBreweryLocationsWithinRangeHandlerTests
|
||||
.Setup(r => r.GetAllLocationsWithinRange(It.IsAny<CoordinateData>(), It.IsAny<double>()))
|
||||
.ReturnsAsync(breweries);
|
||||
|
||||
IEnumerable<SimplifiedBreweryDto> result = await _handler.Handle(
|
||||
IEnumerable<BreweryWithLocationDto> result = await _handler.Handle(
|
||||
new GetBreweryLocationsWithinRangeQuery(43.6532, -79.3832, 1000),
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
@@ -23,3 +23,10 @@ public record CreateBreweryCommand(
|
||||
string Description,
|
||||
CreateBreweryLocation Location
|
||||
) : IRequest<BreweryDto>;
|
||||
|
||||
/// <summary>Request body for <c>POST /api/brewery</c>. <c>PostedById</c> comes from the caller's JWT.</summary>
|
||||
public record CreateBreweryRequest(
|
||||
string BreweryName,
|
||||
string Description,
|
||||
CreateBreweryLocation Location
|
||||
);
|
||||
@@ -37,6 +37,17 @@ public class CreateBreweryHandler(IBreweryRepository repository)
|
||||
};
|
||||
|
||||
await repository.CreateAsync(entity);
|
||||
return entity.ToDto();
|
||||
|
||||
// RowVersion is a DB-computed column, unset on the in-memory entity; re-fetch so callers
|
||||
// get back the value needed for a subsequent optimistic-concurrency update. Uses
|
||||
// CancellationToken.None since the write already committed: cancelling the caller's request
|
||||
// at this point must not turn a successful creation into a thrown exception.
|
||||
BreweryPost created =
|
||||
await repository.GetByIdAsync(entity.BreweryPostId, CancellationToken.None)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Brewery '{entity.BreweryPostId}' was not found after a successful creation."
|
||||
);
|
||||
|
||||
return created.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ using MediatR;
|
||||
|
||||
namespace Features.Breweries.Commands.DeleteBrewery;
|
||||
|
||||
/// <summary>Deletes a brewery post. Bound directly from the route of <c>DELETE /api/brewery/{id}</c>.</summary>
|
||||
public record DeleteBreweryCommand(Guid BreweryPostId) : IRequest;
|
||||
/// <summary>Deletes a brewery post. <see cref="BreweryPostId" /> is bound from the route of <c>DELETE /api/brewery/{id}</c>.</summary>
|
||||
public record DeleteBreweryCommand(Guid BreweryPostId, Guid RequestingUserId) : IRequest;
|
||||
|
||||
@@ -10,8 +10,17 @@ public class DeleteBreweryHandler(IBreweryRepository repository)
|
||||
/// <exception cref="Domain.Exceptions.NotFoundException">
|
||||
/// Thrown when no brewery exists with <paramref name="request" />'s <c>BreweryPostId</c>.
|
||||
/// </exception>
|
||||
public Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
|
||||
/// <exception cref="Domain.Exceptions.ForbiddenException">
|
||||
/// Thrown when <paramref name="request" />'s <c>RequestingUserId</c> did not post the brewery.
|
||||
/// </exception>
|
||||
public async Task Handle(DeleteBreweryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return repository.DeleteAsync(request.BreweryPostId);
|
||||
await repository.EnsureCallerOwnsBreweryAsync(
|
||||
request.BreweryPostId,
|
||||
request.RequestingUserId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
await repository.DeleteAsync(request.BreweryPostId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,25 @@ public record UpdateBreweryLocation(
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing brewery post. Bound directly from the request body of <c>PUT /api/brewery/{id}</c>.
|
||||
/// A <c>null</c> <see cref="Location" /> clears the brewery's location. <see cref="RowVersion" /> must be the
|
||||
/// row-version last read for this brewery (e.g. from a prior <c>GET</c>); the update is rejected with a
|
||||
/// <c>409 Conflict</c> if the brewery was modified since then.
|
||||
/// Updates an existing brewery post. A <c>null</c> <see cref="Location" /> clears the brewery's location.
|
||||
/// <see cref="RowVersion" /> must be the row-version last read for this brewery (e.g. from a prior
|
||||
/// <c>GET</c>); the update is rejected with a <c>409 Conflict</c> if the brewery was modified since then.
|
||||
/// </summary>
|
||||
public record UpdateBreweryCommand(
|
||||
Guid BreweryPostId,
|
||||
Guid RequestingUserId,
|
||||
byte[] RowVersion,
|
||||
Guid PostedById,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
UpdateBreweryLocation? Location
|
||||
) : IRequest<BreweryDto>;
|
||||
|
||||
/// <summary>Request body for <c>PUT /api/brewery/{id}</c>.</summary>
|
||||
public record UpdateBreweryRequest(
|
||||
Guid BreweryPostId,
|
||||
byte[] RowVersion,
|
||||
string BreweryName,
|
||||
string Description,
|
||||
UpdateBreweryLocation? Location
|
||||
);
|
||||
|
||||
|
||||
@@ -26,13 +26,18 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
await repository.EnsureCallerOwnsBreweryAsync(
|
||||
request.BreweryPostId,
|
||||
request.RequestingUserId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
BreweryPost entity = new()
|
||||
{
|
||||
BreweryPostId = request.BreweryPostId,
|
||||
PostedById = request.PostedById,
|
||||
RowVersion = request.RowVersion,
|
||||
BreweryName = request.BreweryName,
|
||||
Description = request.Description,
|
||||
RowVersion = request.RowVersion,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Location = request.Location is null
|
||||
? null
|
||||
@@ -48,7 +53,7 @@ public class UpdateBreweryHandler(IBreweryRepository repository)
|
||||
},
|
||||
};
|
||||
|
||||
BreweryPost updated = await repository.UpdateAsync(entity);
|
||||
BreweryPost updated = await repository.UpdateAsync(entity, cancellationToken);
|
||||
return updated.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +70,13 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
/// <remarks>Allows anonymous access.</remarks>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("locations")]
|
||||
public async Task<ActionResult<ResponseBody<IEnumerable<SimplifiedBreweryDto>>>> GetAllLocations()
|
||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>> GetAllLocations()
|
||||
{
|
||||
IEnumerable<SimplifiedBreweryDto> breweries = await mediator.Send(
|
||||
IEnumerable<BreweryWithLocationDto> breweries = await mediator.Send(
|
||||
new GetAllBreweryLocationsQuery()
|
||||
);
|
||||
return Ok(
|
||||
new ResponseBody<IEnumerable<SimplifiedBreweryDto>>
|
||||
new ResponseBody<IEnumerable<BreweryWithLocationDto>>
|
||||
{
|
||||
Message = "Brewery locations retrieved successfully.",
|
||||
Payload = breweries,
|
||||
@@ -91,17 +91,17 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
/// <remarks>Allows anonymous access.</remarks>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("locations/nearby")]
|
||||
public async Task<ActionResult<ResponseBody<IEnumerable<SimplifiedBreweryDto>>>> GetLocationsWithinRange(
|
||||
public async Task<ActionResult<ResponseBody<IEnumerable<BreweryWithLocationDto>>>> GetLocationsWithinRange(
|
||||
[FromQuery] double latitude,
|
||||
[FromQuery] double longitude,
|
||||
[FromQuery] double rangeInMetres
|
||||
)
|
||||
{
|
||||
IEnumerable<SimplifiedBreweryDto> breweries = await mediator.Send(
|
||||
IEnumerable<BreweryWithLocationDto> breweries = await mediator.Send(
|
||||
new GetBreweryLocationsWithinRangeQuery(latitude, longitude, rangeInMetres)
|
||||
);
|
||||
return Ok(
|
||||
new ResponseBody<IEnumerable<SimplifiedBreweryDto>>
|
||||
new ResponseBody<IEnumerable<BreweryWithLocationDto>>
|
||||
{
|
||||
Message = "Brewery locations retrieved successfully.",
|
||||
Payload = breweries,
|
||||
@@ -113,10 +113,17 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
/// <returns><c>201 Created</c> with the newly created brewery.</returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Create(
|
||||
[FromBody] CreateBreweryCommand command
|
||||
[FromBody] CreateBreweryRequest request
|
||||
)
|
||||
{
|
||||
BreweryDto brewery = await mediator.Send(command);
|
||||
BreweryDto brewery = await mediator.Send(
|
||||
new CreateBreweryCommand(
|
||||
User.GetAuthenticatedUserId(),
|
||||
request.BreweryName,
|
||||
request.Description,
|
||||
request.Location
|
||||
)
|
||||
);
|
||||
return Created(
|
||||
$"/api/brewery/{brewery.BreweryPostId}",
|
||||
new ResponseBody<BreweryDto>
|
||||
@@ -128,27 +135,37 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>Updates an existing brewery post.</summary>
|
||||
/// <param name="id">Must match <paramref name="command" />'s <c>BreweryPostId</c>.</param>
|
||||
/// <param name="id">Must match <paramref name="request" />'s <c>BreweryPostId</c>.</param>
|
||||
/// <returns>
|
||||
/// <c>200 OK</c> with the updated brewery; <c>400 Bad Request</c> if the route ID does not match the
|
||||
/// payload ID; <c>404 Not Found</c> if the brewery or its <c>CityId</c> does not exist; or
|
||||
/// <c>409 Conflict</c> if the brewery was modified since <c>command.RowVersion</c> was read.
|
||||
/// <c>409 Conflict</c> if the brewery was modified since <c>request.RowVersion</c> was read.
|
||||
/// </returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult<ResponseBody<BreweryDto>>> Update(
|
||||
Guid id,
|
||||
[FromBody] UpdateBreweryCommand command
|
||||
[FromBody] UpdateBreweryRequest request
|
||||
)
|
||||
{
|
||||
if (command.BreweryPostId != id)
|
||||
if (request.BreweryPostId != id)
|
||||
return BadRequest(new ResponseBody { Message = "Route ID does not match payload ID." });
|
||||
|
||||
BreweryDto brewery = await mediator.Send(command);
|
||||
BreweryDto breweryUpdated = await mediator.Send(
|
||||
new UpdateBreweryCommand(
|
||||
request.BreweryPostId,
|
||||
User.GetAuthenticatedUserId(),
|
||||
request.RowVersion,
|
||||
request.BreweryName,
|
||||
request.Description,
|
||||
request.Location
|
||||
)
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new ResponseBody<BreweryDto>
|
||||
{
|
||||
Message = "Brewery updated successfully.",
|
||||
Payload = brewery,
|
||||
Payload = breweryUpdated,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -158,7 +175,7 @@ public class BreweryController(IMediator mediator) : ControllerBase
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<ActionResult<ResponseBody>> Delete(Guid id)
|
||||
{
|
||||
await mediator.Send(new DeleteBreweryCommand(id));
|
||||
await mediator.Send(new DeleteBreweryCommand(id, User.GetAuthenticatedUserId()));
|
||||
return Ok(new ResponseBody { Message = "Brewery deleted successfully." });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public record BreweryDto(
|
||||
/// The distance, in metres, from a query's origin point, or <see langword="null"/> for queries with no
|
||||
/// origin point (e.g. <c>GET /api/brewery/locations</c>).
|
||||
/// </param>
|
||||
public record SimplifiedBreweryDto(
|
||||
public record BreweryWithLocationDto(
|
||||
Guid BreweryPostId,
|
||||
string BreweryName,
|
||||
BreweryLocationDto? Location,
|
||||
|
||||
@@ -45,9 +45,9 @@ public static class BreweryDtoMapper
|
||||
/// Converts a <see cref="BreweryPost" /> to its simplified DTO representation, used for
|
||||
/// location-based queries that only need identifying and location details.
|
||||
/// </summary>
|
||||
public static SimplifiedBreweryDto ToSimplifiedDto(this BreweryPost brewery)
|
||||
public static BreweryWithLocationDto ToSimplifiedDto(this BreweryPost brewery)
|
||||
{
|
||||
return new SimplifiedBreweryDto(
|
||||
return new BreweryWithLocationDto(
|
||||
BreweryPostId: brewery.BreweryPostId,
|
||||
BreweryName: brewery.BreweryName,
|
||||
Location: brewery.Location is null
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
||||
|
||||
/// <summary>Handles <see cref="GetAllBreweryLocationsQuery" /> by retrieving all located brewery posts.</summary>
|
||||
public class GetAllBreweryLocationsHandler(IBreweryRepository repository)
|
||||
: IRequestHandler<GetAllBreweryLocationsQuery, IEnumerable<SimplifiedBreweryDto>>
|
||||
: IRequestHandler<GetAllBreweryLocationsQuery, IEnumerable<BreweryWithLocationDto>>
|
||||
{
|
||||
/// <summary>Retrieves every brewery post that has a set location.</summary>
|
||||
public async Task<IEnumerable<SimplifiedBreweryDto>> Handle(
|
||||
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
||||
GetAllBreweryLocationsQuery request,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
|
||||
@@ -7,4 +7,4 @@ namespace Features.Breweries.Queries.GetAllBreweryLocations;
|
||||
/// Retrieves all brewery posts that have a set location. Bound directly from
|
||||
/// <c>GET /api/brewery/locations</c>.
|
||||
/// </summary>
|
||||
public record GetAllBreweryLocationsQuery : IRequest<IEnumerable<SimplifiedBreweryDto>>;
|
||||
public record GetAllBreweryLocationsQuery : IRequest<IEnumerable<BreweryWithLocationDto>>;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class GetBreweryByIdHandler(IBreweryRepository repository)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId);
|
||||
BreweryPost? brewery = await repository.GetByIdAsync(request.BreweryPostId, cancellationToken);
|
||||
return brewery?.ToDto();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
||||
/// Handles <see cref="GetBreweryLocationsWithinRangeQuery" /> by retrieving nearby located brewery posts.
|
||||
/// </summary>
|
||||
public class GetBreweryLocationsWithinRangeHandler(IBreweryRepository repository)
|
||||
: IRequestHandler<GetBreweryLocationsWithinRangeQuery, IEnumerable<SimplifiedBreweryDto>>
|
||||
: IRequestHandler<GetBreweryLocationsWithinRangeQuery, IEnumerable<BreweryWithLocationDto>>
|
||||
{
|
||||
/// <summary>Retrieves brewery posts within range of the requested coordinates, nearest first.</summary>
|
||||
public async Task<IEnumerable<SimplifiedBreweryDto>> Handle(
|
||||
public async Task<IEnumerable<BreweryWithLocationDto>> Handle(
|
||||
GetBreweryLocationsWithinRangeQuery request,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
|
||||
@@ -12,4 +12,4 @@ namespace Features.Breweries.Queries.GetBreweryLocationsWithinRange;
|
||||
/// <param name="Longitude">The origin longitude, in decimal degrees.</param>
|
||||
/// <param name="RangeInMetres">The maximum distance, in metres, from the origin coordinates.</param>
|
||||
public record GetBreweryLocationsWithinRangeQuery(double Latitude, double Longitude, double RangeInMetres)
|
||||
: IRequest<IEnumerable<SimplifiedBreweryDto>>;
|
||||
: IRequest<IEnumerable<BreweryWithLocationDto>>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Domain.Exceptions;
|
||||
|
||||
namespace Features.Breweries.Repository;
|
||||
|
||||
/// <summary>Shared ownership check for commands that mutate a specific brewery post.</summary>
|
||||
public static class BreweryOwnershipExtensions
|
||||
{
|
||||
/// <exception cref="Domain.Exceptions.NotFoundException">Thrown when no brewery exists with the given ID.</exception>
|
||||
/// <exception cref="Domain.Exceptions.ForbiddenException">Thrown when <paramref name="requestingUserId" /> did not post the brewery.</exception>
|
||||
public static async Task EnsureCallerOwnsBreweryAsync(
|
||||
this IBreweryRepository repository,
|
||||
Guid breweryPostId,
|
||||
Guid requestingUserId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
Guid postedById =
|
||||
await repository.GetPostedByIdAsync(breweryPostId, cancellationToken)
|
||||
?? throw new NotFoundException($"Brewery with ID {breweryPostId} not found.");
|
||||
|
||||
if (postedById != requestingUserId)
|
||||
throw new ForbiddenException("You are not authorized to modify this brewery.");
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<BreweryPost?> GetByIdAsync(Guid id)
|
||||
public async Task<BreweryPost?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
IEnumerable<BreweryPost> results = await connection.QueryAsync<
|
||||
@@ -39,43 +39,59 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
Country,
|
||||
BreweryPost
|
||||
>(
|
||||
"""
|
||||
SELECT
|
||||
bp.BreweryPostID,
|
||||
bp.PostedByID,
|
||||
bp.BreweryName,
|
||||
bp.Description,
|
||||
bp.CreatedAt,
|
||||
bp.UpdatedAt,
|
||||
bp.RowVersion,
|
||||
bpl.BreweryPostLocationID,
|
||||
bpl.AddressLine1,
|
||||
bpl.AddressLine2,
|
||||
bpl.PostalCode,
|
||||
CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
|
||||
c.CityID,
|
||||
c.CityName,
|
||||
sp.StateProvinceID,
|
||||
sp.StateProvinceName,
|
||||
sp.ISO3166_2 AS Iso31662,
|
||||
co.CountryID,
|
||||
co.CountryName,
|
||||
co.ISO3166_1 AS Iso31661
|
||||
FROM dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
|
||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||
WHERE bp.BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT
|
||||
bp.BreweryPostID,
|
||||
bp.PostedByID,
|
||||
bp.BreweryName,
|
||||
bp.Description,
|
||||
bp.CreatedAt,
|
||||
bp.UpdatedAt,
|
||||
bp.RowVersion,
|
||||
bpl.BreweryPostLocationID,
|
||||
bpl.AddressLine1,
|
||||
bpl.AddressLine2,
|
||||
bpl.PostalCode,
|
||||
CONVERT(varbinary(max), bpl.Coordinates) AS Coordinates,
|
||||
c.CityID,
|
||||
c.CityName,
|
||||
sp.StateProvinceID,
|
||||
sp.StateProvinceName,
|
||||
sp.ISO3166_2 AS Iso31662,
|
||||
co.CountryID,
|
||||
co.CountryName,
|
||||
co.ISO3166_1 AS Iso31661
|
||||
FROM dbo.BreweryPost bp
|
||||
LEFT JOIN dbo.BreweryPostLocation bpl ON bp.BreweryPostID = bpl.BreweryPostID
|
||||
LEFT JOIN dbo.City c ON bpl.CityID = c.CityID
|
||||
LEFT JOIN dbo.StateProvince sp ON c.StateProvinceID = sp.StateProvinceID
|
||||
LEFT JOIN dbo.Country co ON sp.CountryID = co.CountryID
|
||||
WHERE bp.BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { BreweryPostId = id },
|
||||
cancellationToken: cancellationToken
|
||||
),
|
||||
MapBreweryRow,
|
||||
new { BreweryPostId = id },
|
||||
splitOn: "BreweryPostLocationID,CityID,StateProvinceID,CountryID"
|
||||
);
|
||||
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Guid?> GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
return await connection.ExecuteScalarAsync<Guid?>(
|
||||
new CommandDefinition(
|
||||
"SELECT PostedByID FROM dbo.BreweryPost WHERE BreweryPostID = @BreweryPostId",
|
||||
new { BreweryPostId = id },
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset)
|
||||
{
|
||||
@@ -221,10 +237,10 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery)
|
||||
public async Task<BreweryPost> UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -238,7 +254,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
)
|
||||
is not null;
|
||||
@@ -253,7 +270,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
new CommandDefinition(
|
||||
"SELECT 1 FROM dbo.City WHERE CityID = @CityId",
|
||||
new { brewery.Location.CityId },
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
)
|
||||
is not null;
|
||||
@@ -280,7 +298,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
brewery.Description,
|
||||
brewery.RowVersion,
|
||||
},
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
);
|
||||
|
||||
@@ -300,7 +319,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
WHERE BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -315,7 +335,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
WHERE BreweryPostID = @BreweryPostId
|
||||
""",
|
||||
new { brewery.BreweryPostId },
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
)
|
||||
is not null;
|
||||
@@ -346,7 +367,8 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
Longitude = brewery.Location.Coordinates?.Longitude,
|
||||
Srid = GeographySrid,
|
||||
},
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
);
|
||||
else
|
||||
@@ -385,12 +407,13 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
Longitude = brewery.Location.Coordinates?.Longitude,
|
||||
Srid = GeographySrid,
|
||||
},
|
||||
transaction
|
||||
transaction,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -398,7 +421,7 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
throw;
|
||||
}
|
||||
|
||||
return await GetByIdAsync(brewery.BreweryPostId)
|
||||
return await GetByIdAsync(brewery.BreweryPostId, cancellationToken)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Brewery '{brewery.BreweryPostId}' was not found after a successful update."
|
||||
);
|
||||
@@ -542,25 +565,27 @@ public class BreweryRepository(ISqlConnectionFactory connectionFactory)
|
||||
/// Composes a joined row's <see cref="BreweryPost" />, <see cref="BreweryPostLocation" />,
|
||||
/// <see cref="City" />, <see cref="StateProvince" /> and <see cref="Country" /> fragments into a
|
||||
/// single populated <see cref="BreweryPost" />. When the row has no location (a left-joined
|
||||
/// brewery with no address on file), <paramref name="location" /> is a blank instance produced
|
||||
/// from all-<c>NULL</c> columns; the schema guarantees that whenever a location row was joined
|
||||
/// in, its City/StateProvince/Country chain was too.
|
||||
/// brewery with no address on file), Dapper passes <see langword="null" /> for
|
||||
/// <paramref name="location" /> rather than a blank instance, since its identity column
|
||||
/// (<c>BreweryPostLocationID</c>) is <c>NULL</c>; the schema guarantees that whenever a location
|
||||
/// row was joined in, its City/StateProvince/Country chain was too.
|
||||
/// </summary>
|
||||
private static BreweryPost MapBreweryRow(
|
||||
BreweryPost post,
|
||||
BreweryPostLocation location,
|
||||
City city,
|
||||
StateProvince stateProvince,
|
||||
Country country
|
||||
BreweryPostLocation? location,
|
||||
City? city,
|
||||
StateProvince? stateProvince,
|
||||
Country? country
|
||||
)
|
||||
{
|
||||
if (location.BreweryPostLocationId == Guid.Empty)
|
||||
if (location is null)
|
||||
return post;
|
||||
|
||||
stateProvince.CountryId = country.CountryId;
|
||||
// The schema guarantees that whenever a location row was joined in, its City/StateProvince/
|
||||
// Country chain was too, so these are never null alongside a non-null location.
|
||||
city!.StateProvinceId = stateProvince!.StateProvinceId;
|
||||
stateProvince.CountryId = country!.CountryId;
|
||||
stateProvince.Country = country;
|
||||
|
||||
city.StateProvinceId = stateProvince.StateProvinceId;
|
||||
city.StateProvince = stateProvince;
|
||||
|
||||
location.BreweryPostId = post.BreweryPostId;
|
||||
|
||||
@@ -11,7 +11,13 @@ public interface IBreweryRepository
|
||||
/// Retrieves a brewery post by ID, joined to its location. Returns <c>null</c> if no brewery post
|
||||
/// exists with the given ID, or if it has no associated location.
|
||||
/// </summary>
|
||||
Task<BreweryPost?> GetByIdAsync(Guid id);
|
||||
Task<BreweryPost?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <c>PostedById</c> of the brewery post with the given ID, without joining its
|
||||
/// location. Returns <c>null</c> if no brewery post exists with the given ID.
|
||||
/// </summary>
|
||||
Task<Guid?> GetPostedByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all brewery posts, optionally paginated, ordered by creation date descending. Posts
|
||||
@@ -50,7 +56,7 @@ public interface IBreweryRepository
|
||||
/// Thrown when <paramref name="brewery" />'s <c>RowVersion</c> no longer matches the stored row (it was
|
||||
/// modified by another request since it was last read).
|
||||
/// </exception>
|
||||
Task<BreweryPost> UpdateAsync(BreweryPost brewery);
|
||||
Task<BreweryPost> UpdateAsync(BreweryPost brewery, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Deletes a brewery post by ID. Its location and photos are removed via cascading foreign keys.</summary>
|
||||
/// <exception cref="Domain.Exceptions.NotFoundException">Thrown when no brewery exists with the given <paramref name="id" />.</exception>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using Domain.Exceptions;
|
||||
using Features.Auth.Commands.Account.DeleteAccount;
|
||||
using Features.Auth.Commands.Account.UpdateEmail;
|
||||
using Features.Auth.Commands.Account.UpdatePassword;
|
||||
@@ -31,18 +29,6 @@ namespace Features.Auth.Controllers;
|
||||
[Authorize(AuthenticationSchemes = "JWT")]
|
||||
public class AuthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts the authenticated caller's user ID from the validated access token's <c>sub</c> claim.
|
||||
/// </summary>
|
||||
/// <exception cref="UnauthorizedException">Thrown when the claim is missing or malformed.</exception>
|
||||
private Guid GetAuthenticatedUserId()
|
||||
{
|
||||
string? userIdClaim = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
|
||||
if (!Guid.TryParse(userIdClaim, out Guid userId))
|
||||
throw new UnauthorizedException("Access token is missing a valid user ID claim");
|
||||
return userId;
|
||||
}
|
||||
|
||||
/// <summary>Registers a new user account.</summary>
|
||||
/// <remarks>
|
||||
/// Anonymous access. Returns 201 Created with the new account's ID, username, and issued tokens.
|
||||
@@ -150,7 +136,7 @@ public class AuthController(IMediator mediator) : ControllerBase
|
||||
)
|
||||
{
|
||||
UpdateUsernamePayload payload = await mediator.Send(
|
||||
new UpdateUsernameCommand(GetAuthenticatedUserId(), request.NewUsername)
|
||||
new UpdateUsernameCommand(User.GetAuthenticatedUserId(), request.NewUsername)
|
||||
);
|
||||
return Ok(
|
||||
new ResponseBody<UpdateUsernamePayload>
|
||||
@@ -172,7 +158,7 @@ public class AuthController(IMediator mediator) : ControllerBase
|
||||
)
|
||||
{
|
||||
UpdateEmailPayload payload = await mediator.Send(
|
||||
new UpdateEmailCommand(GetAuthenticatedUserId(), request.NewEmail)
|
||||
new UpdateEmailCommand(User.GetAuthenticatedUserId(), request.NewEmail)
|
||||
);
|
||||
return Ok(
|
||||
new ResponseBody<UpdateEmailPayload>
|
||||
@@ -195,7 +181,7 @@ public class AuthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
UpdatePasswordPayload payload = await mediator.Send(
|
||||
new UpdatePasswordCommand(
|
||||
GetAuthenticatedUserId(),
|
||||
User.GetAuthenticatedUserId(),
|
||||
request.CurrentPassword,
|
||||
request.NewPassword
|
||||
)
|
||||
@@ -218,7 +204,7 @@ public class AuthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
UpdateProfilePayload payload = await mediator.Send(
|
||||
new UpdateProfileCommand(
|
||||
GetAuthenticatedUserId(),
|
||||
User.GetAuthenticatedUserId(),
|
||||
request.FirstName,
|
||||
request.LastName,
|
||||
request.DateOfBirth
|
||||
@@ -241,7 +227,7 @@ public class AuthController(IMediator mediator) : ControllerBase
|
||||
[HttpDelete("account")]
|
||||
public async Task<ActionResult<ResponseBody>> DeleteAccount()
|
||||
{
|
||||
await mediator.Send(new DeleteAccountCommand(GetAuthenticatedUserId()));
|
||||
await mediator.Send(new DeleteAccountCommand(User.GetAuthenticatedUserId()));
|
||||
return Ok(new ResponseBody { Message = "Account deleted successfully." });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user