From 1b6a9027046132d8d29cf7d8922d4012a54fa2db Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 4 Apr 2026 16:26:36 +0300 Subject: [PATCH 01/22] feat(api): add request records --- src/Backend/Services/Polls/Polls.API/Polls.API.csproj | 4 ++++ .../Polls.API/Requests/Cities/CreateCityRequest.cs | 7 +++++++ .../Polls.API/Requests/Cities/UpdateCityRequest.cs | 7 +++++++ .../Polls.API/Requests/Ideas/CreateIdeaRequest.cs | 5 +++++ .../Polls.API/Requests/Ideas/UpdateIdeaRequest.cs | 5 +++++ .../Polls.API/Requests/Polls/CreatePollRequest.cs | 10 ++++++++++ .../Polls.API/Requests/Polls/UpdatePollRequest.cs | 7 +++++++ 7 files changed, 45 insertions(+) create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs diff --git a/src/Backend/Services/Polls/Polls.API/Polls.API.csproj b/src/Backend/Services/Polls/Polls.API/Polls.API.csproj index e40d9b5..a2124e6 100644 --- a/src/Backend/Services/Polls/Polls.API/Polls.API.csproj +++ b/src/Backend/Services/Polls/Polls.API/Polls.API.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs new file mode 100644 index 0000000..d6d9a5b --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs @@ -0,0 +1,7 @@ +using Polls.Application.Cities.DTOs; + +namespace Polls.API.Requests.Cities; + +public record CreateCityRequest( + string Name, + CoordinatesDto Coordinates); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs new file mode 100644 index 0000000..4cbae72 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs @@ -0,0 +1,7 @@ +using Polls.Application.Cities.DTOs; + +namespace Polls.API.Requests.Cities; + +public record UpdateCityRequest( + string Name, + CoordinatesDto Coordinates); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs new file mode 100644 index 0000000..fb350de --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs @@ -0,0 +1,5 @@ +namespace Polls.API.Requests.Ideas; + +public record CreateIdeaRequest( + string Title, + string? Description); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs new file mode 100644 index 0000000..d1a3543 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs @@ -0,0 +1,5 @@ +namespace Polls.API.Requests.Ideas; + +public record UpdateIdeaRequest( + string Title, + string? Description); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs new file mode 100644 index 0000000..4779293 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs @@ -0,0 +1,10 @@ +using Polls.Domain.Polls.Enums; + +namespace Polls.API.Requests.Polls; + +public record CreatePollRequest( + string Title, + string? Description, + PollType Type, + DateTimeOffset EndsAt, + decimal BudgetAmount); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs new file mode 100644 index 0000000..79aa8ca --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs @@ -0,0 +1,7 @@ +namespace Polls.API.Requests.Polls; + +public record UpdatePollRequest( + string Title, + string? Description, + DateTimeOffset EndsAt, + decimal BudgetAmount); From 45d05b921a05ac8cd7d06b2ef5b8a23489a7aeb5 Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 4 Apr 2026 17:32:19 +0300 Subject: [PATCH 02/22] feat(api): add authorization configuration --- .../Polls.API/Authorization/Auth0Settings.cs | 8 +++++ .../ConfigureJwtBearerOptions.cs | 19 ++++++++++++ .../Authorization/PermissionPolicyProvider.cs | 30 +++++++++++++++++++ .../Services/Polls/Polls.API/Polls.API.csproj | 3 +- 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Authorization/PermissionPolicyProvider.cs diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs new file mode 100644 index 0000000..e3e2b4d --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs @@ -0,0 +1,8 @@ +namespace Polls.API.Authorization; + +public class Auth0Settings +{ + public required string Domain { get; set; } + public required string Audience { get; set; } + public required string ClientId { get; set; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs new file mode 100644 index 0000000..420862e --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; + +namespace Polls.API.Authorization; + +public class ConfigureJwtBearerOptions( + IOptions auth0Settings) : IConfigureNamedOptions +{ + private readonly Auth0Settings _auth0Settings = auth0Settings.Value; + + public void Configure(string? name, JwtBearerOptions options) + { + options.Authority = $"https://{_auth0Settings.Domain}/"; + options.Audience = _auth0Settings.Audience; + } + + public void Configure(JwtBearerOptions options) + => Configure(null, options); +} diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/PermissionPolicyProvider.cs b/src/Backend/Services/Polls/Polls.API/Authorization/PermissionPolicyProvider.cs new file mode 100644 index 0000000..fee5a2e --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Authorization/PermissionPolicyProvider.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Authorization; +using Polls.Domain.Authorization; + +namespace Polls.API.Authorization; + +public class PermissionPolicyProvider : IAuthorizationPolicyProvider +{ + public Task GetPolicyAsync(string policyName) + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .RequireClaim(Permissions.ClaimType, policyName) + .Build(); + + return Task.FromResult(policy); + } + + public Task GetDefaultPolicyAsync() + { + return Task.FromResult( + new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build()); + } + + public Task GetFallbackPolicyAsync() + { + return Task.FromResult(null); + } +} diff --git a/src/Backend/Services/Polls/Polls.API/Polls.API.csproj b/src/Backend/Services/Polls/Polls.API/Polls.API.csproj index a2124e6..7c39c80 100644 --- a/src/Backend/Services/Polls/Polls.API/Polls.API.csproj +++ b/src/Backend/Services/Polls/Polls.API/Polls.API.csproj @@ -7,12 +7,13 @@ + - + From a4d85d63acb09c77a99a96e17a61e386520b2673 Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 4 Apr 2026 17:38:39 +0300 Subject: [PATCH 03/22] feat(api): add ResultFilter --- .../Polls.API/Common/Filters/ResultFilter.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs diff --git a/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs new file mode 100644 index 0000000..cdf0e90 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Polls.Domain.Common; +using Polls.Domain.Common.Enums; + +namespace Polls.API.Common.Filters; + +public class ResultFilter : IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) { } + + public void OnActionExecuted(ActionExecutedContext context) + { + if (context.Result is not ObjectResult { Value: Result result }) + return; + + if (result.IsSuccess) + return; + + var statusCode = GetStatusCode(result.Error.Type); + + context.Result = new ObjectResult(new ProblemDetails + { + Status = statusCode, + Title = result.Error.Type.ToString(), + Detail = result.Error.Description, + Instance = context.HttpContext.Request.Path + }) + { + StatusCode = statusCode + }; + } + + private static int GetStatusCode(ErrorType type) => type switch + { + ErrorType.NotFound => StatusCodes.Status404NotFound, + ErrorType.Conflict => StatusCodes.Status409Conflict, + ErrorType.Forbidden => StatusCodes.Status403Forbidden, + ErrorType.Validation => StatusCodes.Status400BadRequest, + _=> StatusCodes.Status400BadRequest + }; +} From 1f018b154f9b028fa939efca0d24a235c9ec1e1e Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 4 Apr 2026 17:45:02 +0300 Subject: [PATCH 04/22] feat(api): add ExceptionHandlerMiddleware --- .../Middleware/ExceptionHandlerMiddleware.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Backend/Services/Polls/Polls.API/Common/Middleware/ExceptionHandlerMiddleware.cs diff --git a/src/Backend/Services/Polls/Polls.API/Common/Middleware/ExceptionHandlerMiddleware.cs b/src/Backend/Services/Polls/Polls.API/Common/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..009fb56 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Common/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Polls.API.Common.Middleware; + +public class ExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger) +{ + private const string ContentType = "application/json"; + private const string InternalServerErrorTitle = "Internal Server Error"; + private const string InternalServerErrorDetail = "An unexpected error occurred."; + + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + logger.LogError( + ex, + "Unhandled exception at {Method} {Path}", + context.Request.Method, + context.Request.Path); + + await HandleExceptionAsync(context); + } + } + + private static async Task HandleExceptionAsync(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = ContentType; + + var problemDetails = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = InternalServerErrorTitle, + Detail = InternalServerErrorDetail, + Instance = context.Request.Path + }; + + await context.Response.WriteAsJsonAsync(problemDetails); + } +} From 679b4f857da046f6299a870b336b164e56dd217c Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 4 Apr 2026 18:17:52 +0300 Subject: [PATCH 05/22] feat(api): add ClaimsPrincipalExtensions --- .../Extensions/ClaimsPrincipalExtensions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Backend/Services/Polls/Polls.API/Common/Extensions/ClaimsPrincipalExtensions.cs diff --git a/src/Backend/Services/Polls/Polls.API/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Backend/Services/Polls/Polls.API/Common/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..32b1a0c --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,24 @@ +using System.Security.Claims; + +namespace Polls.API.Common.Extensions; + +public static class ClaimsPrincipalExtensions +{ + private const string CityIdClaim = "https://citypulse.com/city_id"; + + public static Guid GetUserId(this ClaimsPrincipal user) + { + var claim = user.FindFirstValue(ClaimTypes.NameIdentifier); + return Guid.TryParse(claim, out var id) + ? id + : Guid.Empty; + } + + public static Guid GetCityId(this ClaimsPrincipal user) + { + var claim = user.FindFirstValue(CityIdClaim); + return Guid.TryParse(claim, out var id) + ? id + : Guid.Empty; + } +} From 7c7f1d5f00341e8c64451729ddb4765129506115 Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 5 Apr 2026 10:13:47 +0300 Subject: [PATCH 06/22] feat(api): add admin controolers for cities polls ideas --- .../Admin/AdminCitiesController.cs | 119 ++++++++++++++++ .../Controllers/Admin/AdminIdeasController.cs | 124 +++++++++++++++++ .../Controllers/Admin/AdminPollsController.cs | 127 ++++++++++++++++++ .../Requests/Cities/CreateCityRequest.cs | 5 +- .../Requests/Cities/UpdateCityRequest.cs | 5 +- .../Polls.Domain/Authorization/Permissions.cs | 25 ++-- 6 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs new file mode 100644 index 0000000..27b8b36 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs @@ -0,0 +1,119 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Polls.API.Requests.Cities; +using Polls.Application.Cities.Commands.ChangeStatus; +using Polls.Application.Cities.Commands.CreateCity; +using Polls.Application.Cities.Commands.DeleteCity; +using Polls.Application.Cities.Commands.UpdateCity; +using Polls.Application.Cities.DTOs; +using Polls.Application.Cities.Queries.GetCities; +using Polls.Application.Cities.Queries.GetCityById; +using Polls.Application.Cities.Queries.GetCityWithPolls; +using Polls.Application.Common.Models; +using Polls.Domain.Authorization; +using Polls.Domain.Cities.Enums; +using Polls.Domain.Common; + +namespace Polls.API.Controllers.Admin; + +[ApiController] +[Route("api/v1/admin/cities")] +[Authorize] +public class AdminCitiesController(ISender sender) : ControllerBase +{ + [HttpGet("{id:guid}/polls")] + [Authorize(Policy = Permissions.Cities.ReadAny)] + public async Task> GetCityWithPolls( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetCityWithPollsQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet] + [Authorize(Policy = Permissions.Cities.ReadAny)] + public async Task>> GetCities( + [FromQuery] CityFilter filter, + CancellationToken cancellationToken) + { + var command = new GetCitiesQuery( + Filter: filter, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet("{id:guid}")] + [Authorize(Policy = Permissions.Cities.ReadAny)] + public async Task> GetCityById( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetCityByIdQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost] + [Authorize(Policy = Permissions.Cities.CreateAny)] + public async Task> CreateCity( + CreateCityRequest request, + CancellationToken cancellationToken) + { + var command = new CreateCityCommand( + Title: request.Title, + Coordinates: request.Coordinates, + Description: request.Description); + + return await sender.Send(command, cancellationToken); + } + + [HttpPut("{id:guid}")] + [Authorize(Policy = Permissions.Cities.UpdateAny)] + public async Task> UpdateCity( + Guid id, + UpdateCityRequest request, + CancellationToken cancellationToken) + { + var command = new UpdateCityCommand( + Id: id, + Title: request.Title, + Coordinates: request.Coordinates, + Description: request.Description); + + return await sender.Send(command, cancellationToken); + } + + [HttpDelete("{id:guid}")] + [Authorize(Policy = Permissions.Cities.DeleteAny)] + public async Task> DeleteCity( + Guid id, + CancellationToken cancellationToken) + { + var command = new DeleteCityCommand( + Id: id); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost("{id:guid}/status")] + [Authorize(Policy = Permissions.Cities.ChangeStatusAny)] + public async Task> ChangeStatus( + Guid id, + [FromBody] CityStatus newStatus, + CancellationToken cancellationToken) + { + var command = new ChangeCityStatusCommand( + Id: id, + NewStatus: newStatus); + + return await sender.Send(command, cancellationToken); + } +} diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs new file mode 100644 index 0000000..ac4da25 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs @@ -0,0 +1,124 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Polls.API.Common.Extensions; +using Polls.API.Requests.Ideas; +using Polls.Application.Common.Models; +using Polls.Application.Ideas.Commands.ChangeStatus; +using Polls.Application.Ideas.Commands.CreateIdea; +using Polls.Application.Ideas.Commands.DeleteIdea; +using Polls.Application.Ideas.Commands.UpdateIdea; +using Polls.Application.Ideas.DTOs; +using Polls.Application.Ideas.Queries.GetIdeaById; +using Polls.Application.Ideas.Queries.GetIdeas; +using Polls.Application.Ideas.Queries.GetIdeaWithPoll; +using Polls.Domain.Authorization; +using Polls.Domain.Common; +using Polls.Domain.Ideas.Enums; + +namespace Polls.API.Controllers.Admin; + +[ApiController] +[Route("api/v1/admin/ideas")] +[Authorize] +public class AdminIdeasController(ISender sender) : ControllerBase +{ + [HttpGet] + [Authorize(Policy = Permissions.Ideas.ReadAny)] + public async Task>> GetIdeas( + [FromQuery] IdeaFilter filter, + CancellationToken cancellationToken) + { + var command = new GetIdeasQuery( + Filter: filter, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet("{id:guid}")] + [Authorize(Policy = Permissions.Ideas.ReadAny)] + public async Task> GetIdeaById( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetIdeaByIdQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet("{id:guid}/poll")] + [Authorize(Policy = Permissions.Ideas.ReadAny)] + public async Task> GetIdeaWithPoll( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetIdeaWithPollQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost("{pollId:guid}")] + [Authorize(Policy = Permissions.Ideas.CreateAny)] + public async Task> CreateIdea( + Guid pollId, + CreateIdeaRequest request, + CancellationToken cancellationToken) + { + var command = new CreateIdeaCommand( + UserId: User.GetUserId(), + PollId: pollId, + Title: request.Title, + Description: request.Description, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpPut("{id:guid}")] + [Authorize(Policy = Permissions.Ideas.UpdateAny)] + public async Task> UpdateIdea( + Guid id, + UpdateIdeaRequest request, + CancellationToken cancellationToken) + { + var command = new UpdateIdeaCommand( + Id: id, + Title: request.Title, + Description: request.Description, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpDelete("{id:guid}")] + [Authorize(Policy = Permissions.Ideas.DeleteAny)] + public async Task> DeleteIdea( + Guid id, + CancellationToken cancellationToken) + { + var command = new DeleteIdeaCommand( + Id: id, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost("{id:guid}/status")] + [Authorize(Policy = Permissions.Ideas.ChangeStatusAny)] + public async Task> ChangeStatus( + Guid id, + [FromBody] IdeaStatus newStatus, + CancellationToken cancellationToken) + { + var command = new ChangeIdeaStatusCommand( + Id: id, + NewStatus: newStatus); + + return await sender.Send(command, cancellationToken); + } +} diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs new file mode 100644 index 0000000..50c64cf --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs @@ -0,0 +1,127 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Polls.API.Requests.Polls; +using Polls.Application.Common.Models; +using Polls.Application.Polls.Commands.ChangeStatus; +using Polls.Application.Polls.Commands.CreatePoll; +using Polls.Application.Polls.Commands.DeletePoll; +using Polls.Application.Polls.Commands.UpdatePoll; +using Polls.Application.Polls.DTOs; +using Polls.Application.Polls.Queries.GetPollById; +using Polls.Application.Polls.Queries.GetPolls; +using Polls.Application.Polls.Queries.GetPollWithIdeas; +using Polls.Domain.Authorization; +using Polls.Domain.Common; +using Polls.Domain.Polls.Enums; + +namespace Polls.API.Controllers.Admin; + +[ApiController] +[Route("api/v1/admin/polls")] +[Authorize] +public class AdminPollsController(ISender sender) : ControllerBase +{ + [HttpGet] + [Authorize(Policy = Permissions.Polls.ReadAny)] + public async Task>> GetPolls( + [FromQuery] PollFilter filter, + CancellationToken cancellationToken) + { + var command = new GetPollsQuery( + Filter: filter, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet("{id:guid}")] + [Authorize(Policy = Permissions.Polls.ReadAny)] + public async Task> GetPollById( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetPollByIdQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpGet("{id:guid}/ideas")] + [Authorize(Policy = Permissions.Polls.ReadAny)] + public async Task> GetPollWithIdeas( + Guid id, + CancellationToken cancellationToken) + { + var command = new GetPollWithIdeasQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost("cities/{cityId:guid}")] + [Authorize(Policy = Permissions.Polls.CreateAny)] + public async Task> CreatePoll( + Guid cityId, + CreatePollRequest request, + CancellationToken cancellationToken) + { + var command = new CreatePollCommand( + CityId: cityId, + Title: request.Title, + Description: request.Description, + Type: request.Type, + EndsAt: request.EndsAt, + BudgetAmount: request.BudgetAmount, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpPut("{id:guid}")] + [Authorize(Policy = Permissions.Polls.UpdateAny)] + public async Task> UpdatePoll( + Guid id, + UpdatePollRequest request, + CancellationToken cancellationToken) + { + var command = new UpdatePollCommand( + Id: id, + Title: request.Title, + Description: request.Description, + EndsAt: request.EndsAt, + BudgetAmount: request.BudgetAmount, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpDelete("{id:guid}")] + [Authorize(Policy = Permissions.Polls.DeleteAny)] + public async Task> DeletePoll( + Guid id, + CancellationToken cancellationToken) + { + var command = new DeletePollCommand( + Id: id, + BypassRestrictions: true); + + return await sender.Send(command, cancellationToken); + } + + [HttpPost("{id:guid}/status")] + [Authorize(Policy = Permissions.Polls.ChangeStatusAny)] + public async Task> ChangeStatus( + Guid id, + [FromBody] PollStatus newStatus, + CancellationToken cancellationToken) + { + var command = new ChangePollStatusCommand( + Id: id, + NewStatus: newStatus); + + return await sender.Send(command, cancellationToken); + } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs index d6d9a5b..641d663 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs @@ -3,5 +3,6 @@ namespace Polls.API.Requests.Cities; public record CreateCityRequest( - string Name, - CoordinatesDto Coordinates); + string Title, + CoordinatesDto Coordinates, + string? Description); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs index 4cbae72..a3961aa 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs @@ -3,5 +3,6 @@ namespace Polls.API.Requests.Cities; public record UpdateCityRequest( - string Name, - CoordinatesDto Coordinates); + string Title, + CoordinatesDto Coordinates, + string? Description); diff --git a/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs b/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs index d19a3bf..6fa0eeb 100644 --- a/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs +++ b/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs @@ -7,34 +7,41 @@ public static class Permissions public static class Cities { public const string ReadAny = "cities.read.any"; + public const string ReadActive = "cities.read.active"; public const string CreateAny = "cities.create.any"; public const string UpdateAny = "cities.update.any"; - public const string DeleteAny = "cities.delete.any"; public const string UpdateOwn = "cities.update.own"; + public const string DeleteAny = "cities.delete.any"; + public const string ChangeStatusAny = "cities.changestatus.any"; } public static class Polls { - public const string CreateOwn = "poll.create.own"; - public const string CreateAny = "poll.create.any"; - public const string UpdateCity = "poll.update.city"; - public const string UpdateAny = "poll.update.any"; - public const string DeleteAny = "poll.delete.any"; - public const string VoteCity = "poll.vote.city"; - public const string VoteAny = "poll.vote.any"; + public const string ReadAny = "polls.read.any"; + public const string ReadActive = "polls.read.active"; + public const string CreateOwn = "polls.create.own"; + public const string CreateAny = "polls.create.any"; + public const string UpdateCity = "polls.update.city"; + public const string UpdateAny = "polls.update.any"; + public const string DeleteAny = "polls.delete.any"; + public const string VoteCity = "polls.vote.city"; + public const string VoteAny = "polls.vote.any"; + public const string ChangeStatusAny = "polls.changestatus.any"; } public static class Ideas { + public const string ReadAny = "ideas.read.any"; + public const string ReadActive = "ideas.read.active"; public const string CreateAny = "ideas.create.any"; public const string CreateUserVoting = "ideas.create.user-voting"; public const string CreateManagerVoting = "ideas.create.manager-voting"; - public const string ReadAny = "ideas.read.any"; public const string UpdateAny = "ideas.update.any"; public const string UpdateCity = "ideas.update.city"; public const string UpdateOwn = "ideas.update.own"; public const string DeleteAny = "ideas.delete.any"; public const string DeleteCity = "ideas.delete.city"; public const string DeleteOwn = "ideas.delete.own"; + public const string ChangeStatusAny = "ideas.changestatus.any"; } } From e2f17e52f322b51cc93e72249159a0478b0da47e Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 5 Apr 2026 11:22:14 +0300 Subject: [PATCH 07/22] feat(api): add new permisioins for polls and ideas --- .../Services/Polls/Polls.Domain/Authorization/Permissions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs b/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs index 6fa0eeb..6e96bd2 100644 --- a/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs +++ b/src/Backend/Services/Polls/Polls.Domain/Authorization/Permissions.cs @@ -19,11 +19,12 @@ public static class Polls { public const string ReadAny = "polls.read.any"; public const string ReadActive = "polls.read.active"; - public const string CreateOwn = "polls.create.own"; + public const string CreateCity = "polls.create.city"; public const string CreateAny = "polls.create.any"; public const string UpdateCity = "polls.update.city"; public const string UpdateAny = "polls.update.any"; public const string DeleteAny = "polls.delete.any"; + public const string DeleteCity = "polls.delete.city"; public const string VoteCity = "polls.vote.city"; public const string VoteAny = "polls.vote.any"; public const string ChangeStatusAny = "polls.changestatus.any"; @@ -34,6 +35,7 @@ public static class Ideas public const string ReadAny = "ideas.read.any"; public const string ReadActive = "ideas.read.active"; public const string CreateAny = "ideas.create.any"; + public const string CreateCity = "ideas.create.city"; public const string CreateUserVoting = "ideas.create.user-voting"; public const string CreateManagerVoting = "ideas.create.manager-voting"; public const string UpdateAny = "ideas.update.any"; From a7389bbc7b6616ad1756be2b756371670091019c Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 5 Apr 2026 12:02:30 +0300 Subject: [PATCH 08/22] refactor(api): update formating in admin controllers --- .../Controllers/Admin/AdminCitiesController.cs | 12 ++++++------ .../Controllers/Admin/AdminIdeasController.cs | 12 ++++++------ .../Controllers/Admin/AdminPollsController.cs | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs index 27b8b36..61449f6 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs @@ -28,11 +28,11 @@ public async Task> GetCityWithPolls( Guid id, CancellationToken cancellationToken) { - var command = new GetCityWithPollsQuery( + var query = new GetCityWithPollsQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet] @@ -41,11 +41,11 @@ public async Task>> GetCities( [FromQuery] CityFilter filter, CancellationToken cancellationToken) { - var command = new GetCitiesQuery( + var query = new GetCitiesQuery( Filter: filter, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet("{id:guid}")] @@ -54,11 +54,11 @@ public async Task> GetCityById( Guid id, CancellationToken cancellationToken) { - var command = new GetCityByIdQuery( + var query = new GetCityByIdQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpPost] diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs index ac4da25..72111e0 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs @@ -29,11 +29,11 @@ public async Task>> GetIdeas( [FromQuery] IdeaFilter filter, CancellationToken cancellationToken) { - var command = new GetIdeasQuery( + var query = new GetIdeasQuery( Filter: filter, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet("{id:guid}")] @@ -42,11 +42,11 @@ public async Task> GetIdeaById( Guid id, CancellationToken cancellationToken) { - var command = new GetIdeaByIdQuery( + var query = new GetIdeaByIdQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet("{id:guid}/poll")] @@ -55,11 +55,11 @@ public async Task> GetIdeaWithPoll( Guid id, CancellationToken cancellationToken) { - var command = new GetIdeaWithPollQuery( + var query = new GetIdeaWithPollQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpPost("{pollId:guid}")] diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs index 50c64cf..facc65f 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs @@ -28,11 +28,11 @@ public async Task>> GetPolls( [FromQuery] PollFilter filter, CancellationToken cancellationToken) { - var command = new GetPollsQuery( + var query = new GetPollsQuery( Filter: filter, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet("{id:guid}")] @@ -41,11 +41,11 @@ public async Task> GetPollById( Guid id, CancellationToken cancellationToken) { - var command = new GetPollByIdQuery( + var query = new GetPollByIdQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpGet("{id:guid}/ideas")] @@ -54,11 +54,11 @@ public async Task> GetPollWithIdeas( Guid id, CancellationToken cancellationToken) { - var command = new GetPollWithIdeasQuery( + var query = new GetPollWithIdeasQuery( Id: id, IncludeOnlyActive: false); - return await sender.Send(command, cancellationToken); + return await sender.Send(query, cancellationToken); } [HttpPost("cities/{cityId:guid}")] From 3ba077e339452c1fbd599396e9f380b818248117 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 6 Apr 2026 08:27:59 +0300 Subject: [PATCH 09/22] feat(api): add change status requests to all entities --- .../Admin/AdminCitiesController.cs | 37 ++++++++++--------- .../Controllers/Admin/AdminIdeasController.cs | 6 +-- .../Controllers/Admin/AdminPollsController.cs | 6 +-- .../Cities/ChangeCityStatusRequest.cs | 6 +++ .../Requests/Ideas/ChangeIdeaStatusRequest.cs | 6 +++ .../Requests/Polls/ChangePollStatusRequest.cs | 6 +++ 6 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs create mode 100644 src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs index 61449f6..2f74349 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Polls.API.Requests.Cities; +using Polls.API.Requests.Ideas; using Polls.Application.Cities.Commands.ChangeStatus; using Polls.Application.Cities.Commands.CreateCity; using Polls.Application.Cities.Commands.DeleteCity; @@ -22,19 +23,6 @@ namespace Polls.API.Controllers.Admin; [Authorize] public class AdminCitiesController(ISender sender) : ControllerBase { - [HttpGet("{id:guid}/polls")] - [Authorize(Policy = Permissions.Cities.ReadAny)] - public async Task> GetCityWithPolls( - Guid id, - CancellationToken cancellationToken) - { - var query = new GetCityWithPollsQuery( - Id: id, - IncludeOnlyActive: false); - - return await sender.Send(query, cancellationToken); - } - [HttpGet] [Authorize(Policy = Permissions.Cities.ReadAny)] public async Task>> GetCities( @@ -47,7 +35,7 @@ public async Task>> GetCities( return await sender.Send(query, cancellationToken); } - + [HttpGet("{id:guid}")] [Authorize(Policy = Permissions.Cities.ReadAny)] public async Task> GetCityById( @@ -60,7 +48,20 @@ public async Task> GetCityById( return await sender.Send(query, cancellationToken); } - + + [HttpGet("{id:guid}/polls")] + [Authorize(Policy = Permissions.Cities.ReadAny)] + public async Task> GetCityWithPolls( + Guid id, + CancellationToken cancellationToken) + { + var query = new GetCityWithPollsQuery( + Id: id, + IncludeOnlyActive: false); + + return await sender.Send(query, cancellationToken); + } + [HttpPost] [Authorize(Policy = Permissions.Cities.CreateAny)] public async Task> CreateCity( @@ -103,16 +104,16 @@ public async Task> DeleteCity( return await sender.Send(command, cancellationToken); } - [HttpPost("{id:guid}/status")] + [HttpPatch("{id:guid}/status")] [Authorize(Policy = Permissions.Cities.ChangeStatusAny)] public async Task> ChangeStatus( Guid id, - [FromBody] CityStatus newStatus, + ChangeCityStatusRequest request, CancellationToken cancellationToken) { var command = new ChangeCityStatusCommand( Id: id, - NewStatus: newStatus); + NewStatus: request.NewStatus); return await sender.Send(command, cancellationToken); } diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs index 72111e0..86ecbe5 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs @@ -108,16 +108,16 @@ public async Task> DeleteIdea( return await sender.Send(command, cancellationToken); } - [HttpPost("{id:guid}/status")] + [HttpPatch("{id:guid}/status")] [Authorize(Policy = Permissions.Ideas.ChangeStatusAny)] public async Task> ChangeStatus( Guid id, - [FromBody] IdeaStatus newStatus, + ChangeIdeaStatusRequest request, CancellationToken cancellationToken) { var command = new ChangeIdeaStatusCommand( Id: id, - NewStatus: newStatus); + NewStatus: request.NewStatus); return await sender.Send(command, cancellationToken); } diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs index facc65f..fb43198 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs @@ -111,16 +111,16 @@ public async Task> DeletePoll( return await sender.Send(command, cancellationToken); } - [HttpPost("{id:guid}/status")] + [HttpPatch("{id:guid}/status")] [Authorize(Policy = Permissions.Polls.ChangeStatusAny)] public async Task> ChangeStatus( Guid id, - [FromBody] PollStatus newStatus, + ChangePollStatusRequest request, CancellationToken cancellationToken) { var command = new ChangePollStatusCommand( Id: id, - NewStatus: newStatus); + NewStatus: request.NewStatus); return await sender.Send(command, cancellationToken); } diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs new file mode 100644 index 0000000..c66c590 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs @@ -0,0 +1,6 @@ +using Polls.Domain.Cities.Enums; + +namespace Polls.API.Requests.Cities; + +public record ChangeCityStatusRequest( + CityStatus NewStatus); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs new file mode 100644 index 0000000..9f55d83 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs @@ -0,0 +1,6 @@ +using Polls.Domain.Ideas.Enums; + +namespace Polls.API.Requests.Ideas; + +public record ChangeIdeaStatusRequest( + IdeaStatus NewStatus); diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs new file mode 100644 index 0000000..3f76d00 --- /dev/null +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs @@ -0,0 +1,6 @@ +using Polls.Domain.Polls.Enums; + +namespace Polls.API.Requests.Polls; + +public record ChangePollStatusRequest( + PollStatus NewStatus); From 3c2682ec2cea33b7a5a8a21fa4da3f30afebe0b2 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 9 Apr 2026 11:34:35 +0300 Subject: [PATCH 10/22] feat(application): add Authority to Auth0Settings --- .../Services/Polls/Polls.API/Authorization/Auth0Settings.cs | 1 + .../Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs index e3e2b4d..4ce9b21 100644 --- a/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs +++ b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs @@ -3,6 +3,7 @@ namespace Polls.API.Authorization; public class Auth0Settings { public required string Domain { get; set; } + public required string Authority{ get; set; } public required string Audience { get; set; } public required string ClientId { get; set; } } diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs index 420862e..217dff1 100644 --- a/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs +++ b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs @@ -10,7 +10,7 @@ public class ConfigureJwtBearerOptions( public void Configure(string? name, JwtBearerOptions options) { - options.Authority = $"https://{_auth0Settings.Domain}/"; + options.Authority = _auth0Settings.Authority; options.Audience = _auth0Settings.Audience; } From 43813c072a28b5047bb9d35e71482002158d9bc4 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 9 Apr 2026 11:52:48 +0300 Subject: [PATCH 11/22] refactor(api): use required properties in request DTOs to prevent under-posting --- .../Requests/Cities/ChangeCityStatusRequest.cs | 6 ++++-- .../Polls.API/Requests/Cities/CreateCityRequest.cs | 10 ++++++---- .../Polls.API/Requests/Cities/UpdateCityRequest.cs | 10 ++++++---- .../Requests/Ideas/ChangeIdeaStatusRequest.cs | 6 ++++-- .../Polls.API/Requests/Ideas/CreateIdeaRequest.cs | 8 +++++--- .../Polls.API/Requests/Ideas/UpdateIdeaRequest.cs | 8 +++++--- .../Requests/Polls/ChangePollStatusRequest.cs | 6 ++++-- .../Polls.API/Requests/Polls/CreatePollRequest.cs | 14 ++++++++------ .../Polls.API/Requests/Polls/UpdatePollRequest.cs | 12 +++++++----- 9 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs index c66c590..1d96e47 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/ChangeCityStatusRequest.cs @@ -2,5 +2,7 @@ namespace Polls.API.Requests.Cities; -public record ChangeCityStatusRequest( - CityStatus NewStatus); +public record ChangeCityStatusRequest +{ + public required CityStatus NewStatus { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs index 641d663..417b466 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/CreateCityRequest.cs @@ -2,7 +2,9 @@ namespace Polls.API.Requests.Cities; -public record CreateCityRequest( - string Title, - CoordinatesDto Coordinates, - string? Description); +public record CreateCityRequest +{ + public required string Title { get; init; } + public required CoordinatesDto Coordinates { get; init; } + public string? Description { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs index a3961aa..44b5e00 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Cities/UpdateCityRequest.cs @@ -2,7 +2,9 @@ namespace Polls.API.Requests.Cities; -public record UpdateCityRequest( - string Title, - CoordinatesDto Coordinates, - string? Description); +public record UpdateCityRequest +{ + public required string Title { get; init; } + public required CoordinatesDto Coordinates { get; init; } + public string? Description { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs index 9f55d83..8c66a77 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/ChangeIdeaStatusRequest.cs @@ -2,5 +2,7 @@ namespace Polls.API.Requests.Ideas; -public record ChangeIdeaStatusRequest( - IdeaStatus NewStatus); +public record ChangeIdeaStatusRequest +{ + public required IdeaStatus NewStatus { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs index fb350de..2f6dc5c 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/CreateIdeaRequest.cs @@ -1,5 +1,7 @@ namespace Polls.API.Requests.Ideas; -public record CreateIdeaRequest( - string Title, - string? Description); +public record CreateIdeaRequest +{ + public required string Title { get; init; } + public string? Description { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs index d1a3543..787d570 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Ideas/UpdateIdeaRequest.cs @@ -1,5 +1,7 @@ namespace Polls.API.Requests.Ideas; -public record UpdateIdeaRequest( - string Title, - string? Description); +public record UpdateIdeaRequest +{ + public required string Title { get; init; } + public string? Description { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs index 3f76d00..4bdf046 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/ChangePollStatusRequest.cs @@ -2,5 +2,7 @@ namespace Polls.API.Requests.Polls; -public record ChangePollStatusRequest( - PollStatus NewStatus); +public record ChangePollStatusRequest +{ + public required PollStatus NewStatus { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs index 4779293..ae3243c 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/CreatePollRequest.cs @@ -2,9 +2,11 @@ namespace Polls.API.Requests.Polls; -public record CreatePollRequest( - string Title, - string? Description, - PollType Type, - DateTimeOffset EndsAt, - decimal BudgetAmount); +public record CreatePollRequest +{ + public required string Title { get; init; } + public string? Description { get; init; } + public required PollType Type { get; init; } + public required DateTimeOffset EndsAt { get; init; } + public required decimal BudgetAmount { get; init; } +} diff --git a/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs b/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs index 79aa8ca..f66a6ee 100644 --- a/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs +++ b/src/Backend/Services/Polls/Polls.API/Requests/Polls/UpdatePollRequest.cs @@ -1,7 +1,9 @@ namespace Polls.API.Requests.Polls; -public record UpdatePollRequest( - string Title, - string? Description, - DateTimeOffset EndsAt, - decimal BudgetAmount); +public record UpdatePollRequest +{ + public required string Title { get; init; } + public string? Description { get; init; } + public required DateTimeOffset EndsAt { get; init; } + public required decimal BudgetAmount { get; init; } +} From ee1dea7dfc693d8f2bacd76d42faf940399e81db Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 9 Apr 2026 12:15:14 +0300 Subject: [PATCH 12/22] refactor(api): update formating in admin controllers --- .../Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs | 2 -- .../Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs | 1 - .../Polls/Polls.API/Controllers/Admin/AdminPollsController.cs | 1 - 3 files changed, 4 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs index 2f74349..08f4a83 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminCitiesController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Polls.API.Requests.Cities; -using Polls.API.Requests.Ideas; using Polls.Application.Cities.Commands.ChangeStatus; using Polls.Application.Cities.Commands.CreateCity; using Polls.Application.Cities.Commands.DeleteCity; @@ -13,7 +12,6 @@ using Polls.Application.Cities.Queries.GetCityWithPolls; using Polls.Application.Common.Models; using Polls.Domain.Authorization; -using Polls.Domain.Cities.Enums; using Polls.Domain.Common; namespace Polls.API.Controllers.Admin; diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs index 86ecbe5..d56bae6 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminIdeasController.cs @@ -14,7 +14,6 @@ using Polls.Application.Ideas.Queries.GetIdeaWithPoll; using Polls.Domain.Authorization; using Polls.Domain.Common; -using Polls.Domain.Ideas.Enums; namespace Polls.API.Controllers.Admin; diff --git a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs index fb43198..a65a89b 100644 --- a/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs +++ b/src/Backend/Services/Polls/Polls.API/Controllers/Admin/AdminPollsController.cs @@ -13,7 +13,6 @@ using Polls.Application.Polls.Queries.GetPollWithIdeas; using Polls.Domain.Authorization; using Polls.Domain.Common; -using Polls.Domain.Polls.Enums; namespace Polls.API.Controllers.Admin; From 3793fc78409e1701b01f32f1802213ca07b7f29a Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 9 Apr 2026 15:01:11 +0300 Subject: [PATCH 13/22] fix(api): update ResultFilter to return list of errors --- .../Polls/Polls.API/Common/Filters/ResultFilter.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs index cdf0e90..a985d04 100644 --- a/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs +++ b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs @@ -7,6 +7,8 @@ namespace Polls.API.Common.Filters; public class ResultFilter : IActionFilter { + private const string ErrorsExtensionKey = "errors"; + public void OnActionExecuting(ActionExecutingContext context) { } public void OnActionExecuted(ActionExecutedContext context) @@ -19,13 +21,20 @@ public void OnActionExecuted(ActionExecutedContext context) var statusCode = GetStatusCode(result.Error.Type); - context.Result = new ObjectResult(new ProblemDetails + var problemDetails = new ProblemDetails { Status = statusCode, Title = result.Error.Type.ToString(), Detail = result.Error.Description, Instance = context.HttpContext.Request.Path - }) + }; + + if (result.Errors.Count > 1) + problemDetails.Extensions[ErrorsExtensionKey] = result.Errors + .Select(e => e.Description) + .ToArray(); + + context.Result = new ObjectResult(problemDetails) { StatusCode = statusCode }; From 738ed891205239ff8e348f6913cebe7443f5d19c Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 8 Apr 2026 17:07:52 +0300 Subject: [PATCH 14/22] fix(application): use ICommand instead of IRequest> in ChangeIdeaStatusCommand --- .../Ideas/Commands/ChangeStatus/ChangeIdeaStatusCommand.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Backend/Services/Polls/Polls.Application/Ideas/Commands/ChangeStatus/ChangeIdeaStatusCommand.cs b/src/Backend/Services/Polls/Polls.Application/Ideas/Commands/ChangeStatus/ChangeIdeaStatusCommand.cs index d25441d..14d3e5f 100644 --- a/src/Backend/Services/Polls/Polls.Application/Ideas/Commands/ChangeStatus/ChangeIdeaStatusCommand.cs +++ b/src/Backend/Services/Polls/Polls.Application/Ideas/Commands/ChangeStatus/ChangeIdeaStatusCommand.cs @@ -1,4 +1,5 @@ using MediatR; +using Polls.Application.Common.CQRS; using Polls.Domain.Common; using Polls.Domain.Ideas.Enums; @@ -6,4 +7,4 @@ namespace Polls.Application.Ideas.Commands.ChangeStatus; public sealed record ChangeIdeaStatusCommand( Guid Id, - IdeaStatus NewStatus) : IRequest>; + IdeaStatus NewStatus) : ICommand; From c7c7c84c7b5c6095cc8184c57b15f06135475672 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 6 Apr 2026 11:45:38 +0300 Subject: [PATCH 15/22] fix(infrastructure): replace unsupported Include expression with Where filter --- .../Persistence/Repositories/CityRepository.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/CityRepository.cs b/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/CityRepository.cs index 686696c..e240460 100644 --- a/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/CityRepository.cs +++ b/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/CityRepository.cs @@ -26,9 +26,7 @@ public async Task> GetFilteredAsync( CancellationToken cancellationToken = default) { return await _dbSet - .Include(c => new PollQueryBuilder(c.Polls.AsQueryable()) - .WithStatus(status) - .Build()) + .Include(c => c.Polls.Where(p => status == null || p.Status == status)) .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); } } From e820f85f520202ec8d88dba92200c81ae2bc3315 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 6 Apr 2026 11:53:24 +0300 Subject: [PATCH 16/22] fix(infrastructure): replace unsupported Include expression with Where filter in pollRepository --- .../Persistence/Repositories/PollRepository.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/PollRepository.cs b/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/PollRepository.cs index 01b7d16..7e0eb86 100644 --- a/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/PollRepository.cs +++ b/src/Backend/Services/Polls/Polls.Infrastructure/Persistence/Repositories/PollRepository.cs @@ -29,9 +29,7 @@ public async Task> GetFilteredAsync( CancellationToken cancellationToken = default) { return await _dbSet - .Include(p => new IdeaQueryBuilder(p.Ideas.AsQueryable()) - .WithStatus(ideaStatus) - .Build()) + .Include(p => p.Ideas.Where(i => ideaStatus == null || i.Status == ideaStatus)) .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); } From 8e2077182fc5b9ab57104b98be6260d908934e02 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 6 Apr 2026 12:01:33 +0300 Subject: [PATCH 17/22] refactor(domain): update formating in Error --- src/Backend/Services/Polls/Polls.Domain/Common/Error.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs b/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs index 1d37e02..2fa6d0f 100644 --- a/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs +++ b/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs @@ -13,9 +13,6 @@ public static Error Conflict(string description) => public static Error Forbidden(string description) => new(description, ErrorType.Forbidden); - public static Error Failure(string description) => - new(description, ErrorType.Failure); - public static Error Undefined => new(string.Empty,ErrorType.Undefined); } From d1e1a40bf80264853a63f08e933bbb3c6043b1e6 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 6 Apr 2026 12:06:36 +0300 Subject: [PATCH 18/22] refactor(domain): update formating in Error --- src/Backend/Services/Polls/Polls.Domain/Common/Error.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs b/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs index 2fa6d0f..1d37e02 100644 --- a/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs +++ b/src/Backend/Services/Polls/Polls.Domain/Common/Error.cs @@ -13,6 +13,9 @@ public static Error Conflict(string description) => public static Error Forbidden(string description) => new(description, ErrorType.Forbidden); + public static Error Failure(string description) => + new(description, ErrorType.Failure); + public static Error Undefined => new(string.Empty,ErrorType.Undefined); } From 35a64323eb1dfc7f0ac5b9095c834bce73d70901 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 8 Apr 2026 17:18:01 +0300 Subject: [PATCH 19/22] fix(application): use ICommand instead of IRequest> in ChangePollStatusCommand --- .../Polls/Commands/ChangeStatus/ChangePollStatusCommand.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.Application/Polls/Commands/ChangeStatus/ChangePollStatusCommand.cs b/src/Backend/Services/Polls/Polls.Application/Polls/Commands/ChangeStatus/ChangePollStatusCommand.cs index 70ad05f..694bf45 100644 --- a/src/Backend/Services/Polls/Polls.Application/Polls/Commands/ChangeStatus/ChangePollStatusCommand.cs +++ b/src/Backend/Services/Polls/Polls.Application/Polls/Commands/ChangeStatus/ChangePollStatusCommand.cs @@ -1,9 +1,8 @@ -using MediatR; -using Polls.Domain.Common; +using Polls.Application.Common.CQRS; using Polls.Domain.Polls.Enums; namespace Polls.Application.Polls.Commands.ChangeStatus; public sealed record ChangePollStatusCommand( Guid Id, - PollStatus NewStatus) : IRequest>; + PollStatus NewStatus) : ICommand; From a9ad5f22153a90546db3388b7a7c7aad782b9663 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 16 Apr 2026 15:58:00 +0300 Subject: [PATCH 20/22] refactor(api): remove named overloading on Configure method from ConfigureJwtBearerOptions --- .../Polls.API/Authorization/ConfigureJwtBearerOptions.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs index 217dff1..be9b7ff 100644 --- a/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs +++ b/src/Backend/Services/Polls/Polls.API/Authorization/ConfigureJwtBearerOptions.cs @@ -4,16 +4,13 @@ namespace Polls.API.Authorization; public class ConfigureJwtBearerOptions( - IOptions auth0Settings) : IConfigureNamedOptions + IOptions auth0Settings) : IConfigureOptions { private readonly Auth0Settings _auth0Settings = auth0Settings.Value; - public void Configure(string? name, JwtBearerOptions options) + public void Configure(JwtBearerOptions options) { options.Authority = _auth0Settings.Authority; options.Audience = _auth0Settings.Audience; } - - public void Configure(JwtBearerOptions options) - => Configure(null, options); } From ad2405e2b7e93192fea4ae8e46df00635fc9ce4c Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 16 Apr 2026 16:05:53 +0300 Subject: [PATCH 21/22] refactor(api): update formating in ResultFilter --- .../Services/Polls/Polls.API/Common/Filters/ResultFilter.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs index a985d04..823d4e2 100644 --- a/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs +++ b/src/Backend/Services/Polls/Polls.API/Common/Filters/ResultFilter.cs @@ -13,10 +13,7 @@ public void OnActionExecuting(ActionExecutingContext context) { } public void OnActionExecuted(ActionExecutedContext context) { - if (context.Result is not ObjectResult { Value: Result result }) - return; - - if (result.IsSuccess) + if (context.Result is not ObjectResult { Value: Result { IsSuccess: false } result }) return; var statusCode = GetStatusCode(result.Error.Type); @@ -46,6 +43,7 @@ public void OnActionExecuted(ActionExecutedContext context) ErrorType.Conflict => StatusCodes.Status409Conflict, ErrorType.Forbidden => StatusCodes.Status403Forbidden, ErrorType.Validation => StatusCodes.Status400BadRequest, + _=> StatusCodes.Status400BadRequest }; } From 722b06864c7e3aa1e2bf6f8e42cd229370c33ca8 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 20 Apr 2026 17:03:22 +0300 Subject: [PATCH 22/22] refactor(api): update formating in Auth0Settings --- .../Services/Polls/Polls.API/Authorization/Auth0Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs index 4ce9b21..dc7ae57 100644 --- a/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs +++ b/src/Backend/Services/Polls/Polls.API/Authorization/Auth0Settings.cs @@ -3,7 +3,7 @@ namespace Polls.API.Authorization; public class Auth0Settings { public required string Domain { get; set; } - public required string Authority{ get; set; } + public required string Authority { get; set; } public required string Audience { get; set; } public required string ClientId { get; set; } }