Skip to content

Commit

Permalink
Deploy to PROD (GeoLocation Management and Input-trimming) (#94)
Browse files Browse the repository at this point in the history
* Allow privileged users to manage GeoLocations list. (#91)

* Implement endpoints to CREATE and DELETE geolocations.

* Use the CreateDto in all POST/PUT requests to avoid validation problems. (#92)

* Trim string fields during conversion of request DTOs into entities. (#93)
  • Loading branch information
Zifah authored Aug 17, 2024
1 parent 21ffe70 commit ba73c97
Show file tree
Hide file tree
Showing 23 changed files with 144 additions and 75 deletions.
29 changes: 27 additions & 2 deletions Api/Controllers/GeoLocationsController.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
using Application.Services;
using Api.Utilities;
using Application.Services;
using Core.Dto.Request;
using Core.Dto.Response;
using Core.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;

namespace Api.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
[Authorize(Policy = "AdminAndProLexicographers")]
public class GeoLocationsController : ControllerBase
{
private readonly GeoLocationsService _geoLocationsService;
Expand All @@ -23,11 +27,32 @@ public GeoLocationsController(GeoLocationsService geoLocationsService)
/// An <see cref="GeoLocation[]"/> representing the response containing the list of <see cref="GeoLocation"/> objects.
/// </returns>
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(typeof(GeoLocationDto[]), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ListGeoLocations()
{
var result = (await _geoLocationsService.GetAll()).Select(g => new GeoLocationDto(g.Place, g.Region));
var result = (await _geoLocationsService.GetAll()).Select(g => new GeoLocationDto(g.Id, g.Place, g.Region));
return Ok(result);
}

[HttpPost]
[ProducesResponseType(typeof(GeoLocationDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Create(CreateGeoLocationDto geo)
{
var geoLocation = new GeoLocation(geo.Place, geo.Region)
{
CreatedBy = User!.Identity!.Name!
};
await _geoLocationsService.Create(geoLocation);
return StatusCode((int)HttpStatusCode.Created, ResponseHelper.GetResponseDict("Geolocation successfully added"));
}

[HttpDelete("{id}/{place}")]
[ProducesResponseType(typeof(GeoLocationDto), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(string id, string place)
{
await _geoLocationsService.Delete(id, place);
return Ok(ResponseHelper.GetResponseDict($"Geolocation '{place}' successfully deleted"));
}
}
}
5 changes: 3 additions & 2 deletions Api/Controllers/SuggestedNameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using Application.Mappers;
using Application.Validation;
using FluentValidation;

namespace Api.Controllers;

Expand All @@ -16,9 +17,9 @@ namespace Api.Controllers;
public class SuggestedNameController : ControllerBase
{
private readonly SuggestedNameService _suggestedNameService;
private readonly CreateSuggestedNameValidator _suggestedNameValidator;
private readonly IValidator<CreateSuggestedNameDto> _suggestedNameValidator;

public SuggestedNameController(SuggestedNameService suggestedNameService, CreateSuggestedNameValidator suggestedNameValidator)
public SuggestedNameController(SuggestedNameService suggestedNameService, IValidator<CreateSuggestedNameDto> suggestedNameValidator)
{
_suggestedNameService = suggestedNameService;
_suggestedNameValidator = suggestedNameValidator;
Expand Down
11 changes: 4 additions & 7 deletions Api/ExceptionHandler/GlobalExceptionHandling.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Api.ExceptionHandler
{
using Api.Utilities;
using Application.Exceptions;
using System.Net;
using System.Text.Json;
Expand Down Expand Up @@ -32,21 +33,17 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception
context.Response.ContentType = "application/json";
var response = context.Response;

var errorResponse = new ErrorResponse
{
Success = false
};
Dictionary<string, string> errorResponse;
switch (exception)
{
case ClientException ex:
response.StatusCode = (int)HttpStatusCode.BadRequest;
errorResponse.Message = ex.Message;
errorResponse = ResponseHelper.GetResponseDict(ex.Message);
break;


default:
response.StatusCode = (int)HttpStatusCode.InternalServerError;
errorResponse.Message = "Internal server error!";
errorResponse = ResponseHelper.GetResponseDict("Internal server error!");
break;
}
_logger.LogError(exception, "Unhandled Application Exception");
Expand Down
4 changes: 2 additions & 2 deletions Application/Mappers/NameEntryMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static NameEntry MapToEntity(this NameDto request)
{
return new NameEntry
{
Name = request.Name,
Name = request.Name.Trim(),
Pronunciation = request.Pronunciation?.Trim(),
Meaning = request.Meaning.Trim(),
ExtendedMeaning = request.ExtendedMeaning?.Trim(),
Expand Down Expand Up @@ -59,7 +59,7 @@ public static NameEntryDto MapToDto(this NameEntry nameEntry)
Meaning = nameEntry.Meaning,
ExtendedMeaning = nameEntry.ExtendedMeaning,
Morphology = (CommaSeparatedString)nameEntry.Morphology,
GeoLocation = nameEntry.GeoLocation.Select(ge => new GeoLocationDto(ge.Place, ge.Region)).ToList(),
GeoLocation = nameEntry.GeoLocation.Select(ge => new GeoLocationDto(ge.Id, ge.Place, ge.Region)).ToList(),
FamousPeople = (CommaSeparatedString)nameEntry.FamousPeople,
Media = (CommaSeparatedString)nameEntry.Media,
SubmittedBy = nameEntry.CreatedBy,
Expand Down
8 changes: 4 additions & 4 deletions Application/Mappers/SuggestedNameMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public static SuggestedName MapToEntity(this CreateSuggestedNameDto request)
return new SuggestedName
{
Id = ObjectId.GenerateNewId().ToString(),
Name = request.Name,
Email = request.Email,
Details = request.Details,
Name = request.Name.Trim(),
Email = request.Email?.Trim(),
Details = request.Details?.Trim(),
GeoLocation = request.GeoLocation.Select(x => new GeoLocation
{
Place = x.Place,
Expand All @@ -36,7 +36,7 @@ public static SuggestedNameDto MapToDto(this SuggestedName request)
Name = request.Name,
Email = request.Email,
Details = request.Details,
GeoLocation = request.GeoLocation.Select(ge => new GeoLocationDto(ge.Place, ge.Region)).ToList(),
GeoLocation = request.GeoLocation.Select(ge => new GeoLocationDto(ge.Id, ge.Place, ge.Region)).ToList(),
};
}
}
35 changes: 32 additions & 3 deletions Application/Services/GeoLocationsService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Core.Entities;
using Application.Exceptions;
using Core.Entities;
using Core.Repositories;
using Application.Exceptions;

namespace Application.Services
{
Expand All @@ -14,7 +14,36 @@ public GeoLocationsService(IGeoLocationsRepository geoLocationsRepository)
}
public async Task<List<GeoLocation>> GetAll()
{
return await _geoLocationsRepository.GetAll();
return await _geoLocationsRepository.GetAll();
}
public async Task Create(GeoLocation geoLocation)
{
var match = await _geoLocationsRepository.FindByPlace(geoLocation.Place);
if (match != null)
{
throw new ClientException("This location already exists.");
}

await _geoLocationsRepository.Create(new GeoLocation
{
Place = geoLocation.Place.Trim().ToUpper(),
Region = geoLocation.Region.Trim().ToUpper(),
CreatedBy = geoLocation.CreatedBy,
});
}

public async Task Delete(string id, string place)
{
if (string.IsNullOrWhiteSpace(place) || string.IsNullOrWhiteSpace(id))
{
throw new ClientException("One or more input parameters are not valid.");
}

var deleteCount = await _geoLocationsRepository.Delete(id, place);
if (deleteCount == 0)
{
throw new ClientException("No matching records were found to delete.");
}
}
}
}
2 changes: 1 addition & 1 deletion Application/Services/NameEntryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task Create(NameEntry entry)
{
existingName.Duplicates.Add(entry);
await UpdateName(existingName);
_logger.LogWarning($"Someone attempted to create a new name over existing name: {name}.");
_logger.LogWarning("Someone attempted to create a new name over existing name: {name}.", name);
return;
}

Expand Down
2 changes: 0 additions & 2 deletions Application/Validation/CreateSuggestedNameValidator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Core.Dto.Request;
using Core.Dto.Response;
using Core.Enums;
using FluentValidation;

namespace Application.Validation
Expand Down
7 changes: 1 addition & 6 deletions Application/Validation/GeoLocationValidator.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
using Core.Dto.Request;
using Core.Repositories;
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Application.Validation
{
public class GeoLocationValidator : AbstractValidator<GeoLocationDto>
public class GeoLocationValidator : AbstractValidator<CreateGeoLocationDto>
{
private readonly IGeoLocationsRepository _geoLocationsRepository;

Expand Down
6 changes: 1 addition & 5 deletions Application/Validation/NameValidator.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using Core.Dto;
using Core.Dto.Request;
using Core.Dto.Request;
using FluentValidation;
namespace Application.Validation
{
public class NameValidator : AbstractValidator<NameDto>
{



public NameValidator(GeoLocationValidator geoLocationValidator, EmbeddedVideoValidator embeddedVideoValidator, EtymologyValidator etymologyValidator)
{
RuleLevelCascadeMode = CascadeMode.Stop;
Expand Down
2 changes: 1 addition & 1 deletion Core/Dto/CharacterSeparatedString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public abstract class CharacterSeparatedString<T> where T : CharacterSeparatedSt

public CharacterSeparatedString(string? value)

Check warning on line 9 in Core/Dto/CharacterSeparatedString.cs

View workflow job for this annotation

GitHub Actions / deploy / deploy

Non-nullable property 'SeparatorOut' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
this.value = value ?? string.Empty;
this.value = value?.Trim() ?? string.Empty;
}

public override string ToString()
Expand Down
6 changes: 6 additions & 0 deletions Core/Dto/Request/CreateGeoLocationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Core.Dto.Request
{
public record CreateGeoLocationDto(string Place, string Region)
{
}
}
7 changes: 3 additions & 4 deletions Core/Dto/Request/CreateSuggestedNameDto.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@

namespace Core.Dto.Request;
namespace Core.Dto.Request;

public record CreateSuggestedNameDto
{
public string Name { get; init; }
public string Details { get; init; }
public string Email { get; init; }
public List<GeoLocationDto> GeoLocation { get; set; }
public List<CreateGeoLocationDto> GeoLocation { get; set; }
public CreateSuggestedNameDto()
{
GeoLocation = new List<GeoLocationDto>();
GeoLocation = new List<CreateGeoLocationDto>();
}

}
11 changes: 0 additions & 11 deletions Core/Dto/Request/GeoLocationDto.cs

This file was deleted.

7 changes: 3 additions & 4 deletions Core/Dto/Request/NameDto.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Core.Enums;
using System.ComponentModel.DataAnnotations;

namespace Core.Dto.Request
{
Expand All @@ -13,7 +12,7 @@ public abstract class NameDto
public State? State { get; set; }
public List<EtymologyDto> Etymology { get; set; }
public List<EmbeddedVideoDto> Videos { get; set; }
public List<GeoLocationDto> GeoLocation { get; set; }
public List<CreateGeoLocationDto> GeoLocation { get; set; }

public CommaSeparatedString? FamousPeople { get; set; }

Expand All @@ -33,14 +32,14 @@ public NameDto(string name, string meaning)
Meaning = meaning ?? throw new ArgumentNullException(nameof(meaning));
Etymology = new List<EtymologyDto>();
Videos = new List<EmbeddedVideoDto>();
GeoLocation = new List<GeoLocationDto>();
GeoLocation = new List<CreateGeoLocationDto>();
}

public NameDto()
{
Etymology = new List<EtymologyDto>();
Videos = new List<EmbeddedVideoDto>();
GeoLocation = new List<GeoLocationDto>();
GeoLocation = new List<CreateGeoLocationDto>();
}
}
}
10 changes: 10 additions & 0 deletions Core/Dto/Response/GeoLocationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Core.Dto.Response
{
public record GeoLocationDto(string Id, string Place, string Region)
{
public override string ToString()
{
return Place;
}
}
}
4 changes: 1 addition & 3 deletions Core/Dto/Response/SuggestedNameDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Core.Dto.Request;

namespace Core.Dto.Response;
namespace Core.Dto.Response;

public record SuggestedNameDto
{
Expand Down
7 changes: 2 additions & 5 deletions Core/Repositories/IGeoLocationsRepository.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using Core.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Repositories
{
Expand All @@ -13,5 +8,7 @@ public interface IGeoLocationsRepository

Task<GeoLocation> FindByPlace(string place);
Task<GeoLocation> FindByPlaceAndRegion(string region, string place);
Task Create(GeoLocation geoLocation);
Task<int> Delete(string id, string place);
}
}
Loading

0 comments on commit ba73c97

Please sign in to comment.