Compare commits
13 commits
add-licens
...
main
Author | SHA1 | Date | |
---|---|---|---|
84224a1120 | |||
e193d5394d | |||
47d7706cd2 | |||
86fbe4d2da | |||
b3d4df5a81 | |||
3a0e59604b | |||
bf431cb3e4 | |||
127652002b | |||
cf10df84f2 | |||
febccc89d2 | |||
db1008eb2f | |||
fded710658 | |||
b9b9f46ede |
20 changed files with 573 additions and 143 deletions
21
Readme.md
21
Readme.md
|
@ -1,6 +1,7 @@
|
|||
# Introduction
|
||||
|
||||
![Build Status](https://circleci.com/gh/dnutiu/retroactiune.svg?style=svg)
|
||||
![Build Status](https://circleci.com/gh/dnutiu/retroactiune.svg?style=svg)
|
||||
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dnutiu/retroactiune) ![GitHub repo size](https://img.shields.io/github/repo-size/dnutiu/retroactiune) ![GitHub top language](https://img.shields.io/github/languages/top/dnutiu/retroactiune)
|
||||
|
||||
![Swagger API](./docs/retroactiune_swagger.png)
|
||||
|
||||
|
@ -12,7 +13,7 @@ The given Feedback is anonymous by design.
|
|||
|
||||
## Tech Stack
|
||||
|
||||
The project uses ASP .Net Core 3.1 and [MongoDB](https://www.mongodb.com/).
|
||||
The project uses [ASP .Net Core 3.1](https://docs.microsoft.com/en-us/aspnet/core/) and [MongoDB](https://www.mongodb.com/).
|
||||
|
||||
```bash
|
||||
dotnet --version
|
||||
|
@ -30,8 +31,18 @@ The application code is organized using the [Clean Architecture](https://docs.mi
|
|||
|
||||
![Example deployment architecture](./docs/app_architecture_layers.png)
|
||||
|
||||
## Developing
|
||||
## Authorization Provider
|
||||
|
||||
An external Authorization provider is required in order to run the API, the provider needs to support
|
||||
Bearer tokens and HS256 key signing algorithm. _RS256 is currently not supported._
|
||||
|
||||
I recommend that you start with [Auth0](https://auth0.com/) and their free tier.
|
||||
|
||||
See the following resources:
|
||||
- https://auth0.com/docs/get-started/set-up-apis
|
||||
- https://developers.redhat.com/blog/2020/01/29/api-login-and-jwt-token-generation-using-keycloak#set_up_a_client
|
||||
|
||||
## Developing
|
||||
|
||||
To install the dependencies run `dotnet restore`.
|
||||
|
||||
|
@ -48,4 +59,6 @@ _Note: [Docker](https://www.docker.com/) and [Docker-Compose](https://docs.docke
|
|||
```bash
|
||||
docker-compose up -d
|
||||
dotnet test
|
||||
```
|
||||
```
|
||||
|
||||
The projects has ~96% code coverage.
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
@ -10,13 +11,12 @@ namespace Retroactiune.Core.Entities
|
|||
/// </summary>
|
||||
public class Feedback
|
||||
{
|
||||
|
||||
public Feedback()
|
||||
{
|
||||
Id = ObjectId.GenerateNewId().ToString();
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
|
||||
[BsonId, JsonPropertyName("id")]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; }
|
||||
|
@ -24,12 +24,31 @@ namespace Retroactiune.Core.Entities
|
|||
[JsonPropertyName("feedback_receiver_id")]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string FeedbackReceiverId { get; set; }
|
||||
|
||||
[JsonPropertyName("rating")]
|
||||
public uint Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("rating")] public uint Rating { get; set; }
|
||||
|
||||
[JsonPropertyName("description")] public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
private bool Equals(Feedback other)
|
||||
{
|
||||
return Id == other.Id && FeedbackReceiverId == other.FeedbackReceiverId && Rating == other.Rating &&
|
||||
Description == other.Description && CreatedAt - other.CreatedAt < TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != this.GetType()) return false;
|
||||
return Equals((Feedback) obj);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id, FeedbackReceiverId, Rating, Description, CreatedAt);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,16 @@ namespace Retroactiune.Core.Entities
|
|||
|
||||
[JsonPropertyName("expiry_time")] public DateTime? ExpiryTime { get; set; }
|
||||
|
||||
public static bool operator ==(Token left, Token right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(Token left, Token right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is Token convertedObj))
|
||||
|
@ -50,13 +60,18 @@ namespace Retroactiune.Core.Entities
|
|||
return RuntimeHelpers.GetHashCode(this);
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
var hasExpired = ExpiryTime != null && ExpiryTime <= DateTime.UtcNow;
|
||||
var isUsed = TimeUsed != null;
|
||||
return !(hasExpired || isUsed);
|
||||
}
|
||||
|
||||
public bool IsValid(FeedbackReceiver feedbackReceiver)
|
||||
{
|
||||
Guard.Against.Null(feedbackReceiver, nameof(feedbackReceiver));
|
||||
var hasExpired = ExpiryTime != null && ExpiryTime <= DateTime.UtcNow;
|
||||
var differentFeedbackReceiver = !FeedbackReceiverId.Equals(feedbackReceiver.Id);
|
||||
var isUsed = TimeUsed != null;
|
||||
return !(hasExpired || differentFeedbackReceiver || isUsed);
|
||||
return !differentFeedbackReceiver && IsValid();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Retroactiune.Core.Services
|
||||
{
|
||||
|
@ -7,12 +8,11 @@ namespace Retroactiune.Core.Services
|
|||
/// </summary>
|
||||
public class FeedbacksListFilters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// FeedbackReceiverId the ID of the FeedbackReceiver.
|
||||
/// </summary>
|
||||
public string FeedbackReceiverId { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// CreatedAfter filters items that have been created after the given date.
|
||||
/// </summary>
|
||||
|
@ -27,5 +27,23 @@ namespace Retroactiune.Core.Services
|
|||
/// Rating filters for the rating.
|
||||
/// </summary>
|
||||
public uint Rating { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj != null && Equals((FeedbacksListFilters) obj);
|
||||
}
|
||||
|
||||
private bool Equals(FeedbacksListFilters other)
|
||||
{
|
||||
return FeedbackReceiverId == other.FeedbackReceiverId &&
|
||||
Nullable.Equals(CreatedAfter, other.CreatedAfter) &&
|
||||
Nullable.Equals(CreatedBefore, other.CreatedBefore) && Rating == other.Rating;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(FeedbackReceiverId, CreatedAfter, CreatedBefore, Rating);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,6 @@ namespace Retroactiune.Core.Services
|
|||
|
||||
public async Task<IEnumerable<Feedback>> GetFeedbacksAsync(FeedbacksListFilters filters)
|
||||
{
|
||||
// TODO: Unit test.
|
||||
Guard.Against.Null(filters, nameof(filters));
|
||||
Guard.Against.Null(filters.FeedbackReceiverId, nameof(filters.FeedbackReceiverId));
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.15" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.2.0" />
|
||||
|
|
|
@ -435,15 +435,42 @@ namespace Retroactiune.IntegrationTests.Retroactiune.WebAPI.Controllers
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var feedbacksCursor = await _mongoDb.FeedbacksCollection.FindAsync(FilterDefinition<Feedback>.Empty);
|
||||
var feedbacks = await feedbacksCursor.ToListAsync();
|
||||
|
||||
|
||||
Assert.Equal("ok", feedbacks.ElementAt(0).Description);
|
||||
Assert.Equal(4u, feedbacks.ElementAt(0).Rating);
|
||||
Assert.Equal(feedbackReceiver.Id, feedbacks.ElementAt(0).FeedbackReceiverId);
|
||||
|
||||
|
||||
var tokensCursor = await _mongoDb.TokensCollection.FindAsync(FilterDefinition<Token>.Empty);
|
||||
var tokens = await tokensCursor.ToListAsync();
|
||||
|
||||
|
||||
Assert.NotNull(tokens.ElementAt(0).TimeUsed);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Test_GetFeedbacks(IEnumerable<Feedback> feedbacksSeed)
|
||||
{
|
||||
// Setup
|
||||
await _mongoDb.DropAsync();
|
||||
var feedbackReceiverGuid = ObjectId.GenerateNewId().ToString();
|
||||
var selectedFeedbacksSeed = feedbacksSeed.Select(i =>
|
||||
{
|
||||
i.Id = ObjectId.GenerateNewId().ToString();
|
||||
i.CreatedAt = i.CreatedAt.ToUniversalTime();
|
||||
i.FeedbackReceiverId = feedbackReceiverGuid;
|
||||
return i;
|
||||
}).ToList();
|
||||
|
||||
await _mongoDb.FeedbacksCollection.InsertManyAsync(selectedFeedbacksSeed);
|
||||
|
||||
// Test
|
||||
var response = await _client.GetAsync($"/api/v1/feedback_receivers/{feedbackReceiverGuid}/feedbacks");
|
||||
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var feedbacksResponse =
|
||||
JsonSerializer.Deserialize<List<Feedback>>(await response.Content.ReadAsStringAsync());
|
||||
Assert.Equal(selectedFeedbacksSeed,feedbacksResponse);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -81,5 +81,110 @@ namespace Retroactiune.Tests.Retroactiune.Core.Services
|
|||
It.IsAny<InsertOneOptions>(),
|
||||
It.IsAny<CancellationToken>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_GetFeedbacksAsync_NullGuards()
|
||||
{
|
||||
// Setup
|
||||
var mongoDatabaseMock = new Mock<IMongoDatabase>();
|
||||
var mongoClientMock = new Mock<IMongoClient>();
|
||||
var mongoSettingsMock = new Mock<IDatabaseSettings>();
|
||||
var mongoCollectionMock = new Mock<IMongoCollection<Feedback>>();
|
||||
|
||||
mongoSettingsMock.SetupGet(i => i.DatabaseName).Returns("MyDB");
|
||||
mongoSettingsMock.SetupGet(i => i.FeedbacksCollectionName).Returns("feedbacks");
|
||||
|
||||
mongoClientMock
|
||||
.Setup(stub => stub.GetDatabase(It.IsAny<string>(),
|
||||
It.IsAny<MongoDatabaseSettings>()))
|
||||
.Returns(mongoDatabaseMock.Object);
|
||||
|
||||
mongoDatabaseMock
|
||||
.Setup(i => i.GetCollection<Feedback>(It.IsAny<string>(),
|
||||
It.IsAny<MongoCollectionSettings>()))
|
||||
.Returns(mongoCollectionMock.Object);
|
||||
|
||||
// Test & Assert
|
||||
var service = new FeedbacksService(mongoClientMock.Object, mongoSettingsMock.Object);
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => { await service.GetFeedbacksAsync(null); });
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
||||
{
|
||||
await service.GetFeedbacksAsync(new FeedbacksListFilters());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Test_GetFeedbacksAsync_Happy(FeedbacksListFilters feedbacksListFilters)
|
||||
{
|
||||
// Setup
|
||||
var mongoDatabaseMock = new Mock<IMongoDatabase>();
|
||||
var mongoClientMock = new Mock<IMongoClient>();
|
||||
var mongoSettingsMock = new Mock<IDatabaseSettings>();
|
||||
var mongoCollectionMock = new Mock<IMongoCollection<Feedback>>();
|
||||
|
||||
mongoSettingsMock.SetupGet(i => i.DatabaseName).Returns("MyDB");
|
||||
mongoSettingsMock.SetupGet(i => i.FeedbacksCollectionName).Returns("feedbacks");
|
||||
|
||||
mongoClientMock
|
||||
.Setup(stub => stub.GetDatabase(It.IsAny<string>(),
|
||||
It.IsAny<MongoDatabaseSettings>()))
|
||||
.Returns(mongoDatabaseMock.Object);
|
||||
|
||||
mongoDatabaseMock
|
||||
.Setup(i => i.GetCollection<Feedback>(It.IsAny<string>(),
|
||||
It.IsAny<MongoCollectionSettings>()))
|
||||
.Returns(mongoCollectionMock.Object);
|
||||
|
||||
mongoCollectionMock.Setup(i => i.FindAsync(It.IsAny<FilterDefinition<Feedback>>(),
|
||||
It.IsAny<FindOptions<Feedback, Feedback>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new Mock<IAsyncCursor<Feedback>>().Object);
|
||||
|
||||
// Test
|
||||
var service = new FeedbacksService(mongoClientMock.Object, mongoSettingsMock.Object);
|
||||
await service.GetFeedbacksAsync(feedbacksListFilters);
|
||||
|
||||
// Assert
|
||||
mongoCollectionMock.Verify(i => i.FindAsync(It.IsAny<FilterDefinition<Feedback>>(),
|
||||
It.IsAny<FindOptions<Feedback, Feedback>>(), It.IsAny<CancellationToken>()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task Test_GetFeedbacksAsync_Happy_MinimalFilters(string feedbackReceiverId)
|
||||
{
|
||||
// Setup
|
||||
var mongoDatabaseMock = new Mock<IMongoDatabase>();
|
||||
var mongoClientMock = new Mock<IMongoClient>();
|
||||
var mongoSettingsMock = new Mock<IDatabaseSettings>();
|
||||
var mongoCollectionMock = new Mock<IMongoCollection<Feedback>>();
|
||||
|
||||
mongoSettingsMock.SetupGet(i => i.DatabaseName).Returns("MyDB");
|
||||
mongoSettingsMock.SetupGet(i => i.FeedbacksCollectionName).Returns("feedbacks");
|
||||
|
||||
mongoClientMock
|
||||
.Setup(stub => stub.GetDatabase(It.IsAny<string>(),
|
||||
It.IsAny<MongoDatabaseSettings>()))
|
||||
.Returns(mongoDatabaseMock.Object);
|
||||
|
||||
mongoDatabaseMock
|
||||
.Setup(i => i.GetCollection<Feedback>(It.IsAny<string>(),
|
||||
It.IsAny<MongoCollectionSettings>()))
|
||||
.Returns(mongoCollectionMock.Object);
|
||||
|
||||
mongoCollectionMock.Setup(i => i.FindAsync(It.IsAny<FilterDefinition<Feedback>>(),
|
||||
It.IsAny<FindOptions<Feedback, Feedback>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new Mock<IAsyncCursor<Feedback>>().Object);
|
||||
|
||||
// Test
|
||||
var service = new FeedbacksService(mongoClientMock.Object, mongoSettingsMock.Object);
|
||||
await service.GetFeedbacksAsync(new FeedbacksListFilters()
|
||||
{
|
||||
FeedbackReceiverId = feedbackReceiverId
|
||||
});
|
||||
|
||||
// Assert
|
||||
mongoCollectionMock.Verify(i => i.FindAsync(It.IsAny<FilterDefinition<Feedback>>(),
|
||||
It.IsAny<FindOptions<Feedback, Feedback>>(), It.IsAny<CancellationToken>()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -202,9 +202,6 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
mockService.Verify(s => s.FindAsync(filterArr, offset, limit), Times.Once);
|
||||
}
|
||||
|
||||
// Invalid token
|
||||
// happy
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AddFeedback_No_FeedbackReceiver(FeedbackInDto requestBody)
|
||||
{
|
||||
|
@ -272,7 +269,7 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
TimeUsed = DateTime.UtcNow
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Test
|
||||
var controller = new FeedbackReceiversController(feedbackReceiversService.Object, tokensService.Object,
|
||||
feedbacksService.Object, mapper, null,
|
||||
|
@ -282,8 +279,8 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
// Assert
|
||||
Assert.IsType<BadRequestObjectResult>(result);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AddFeedback_Happy(FeedbackInDto requestBody)
|
||||
{
|
||||
|
@ -295,11 +292,15 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
var logger = new Mock<ILogger<FeedbackReceiversController>>();
|
||||
|
||||
feedbackReceiversService
|
||||
.Setup(i => i.FindAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<int?>(), It.IsAny<int?>()))
|
||||
.ReturnsAsync(new[] {new FeedbackReceiver
|
||||
.Setup(i => i.FindAsync(It.IsAny<IEnumerable<string>>(),
|
||||
It.IsAny<int?>(), It.IsAny<int?>()))
|
||||
.ReturnsAsync(new[]
|
||||
{
|
||||
Id = "batman"
|
||||
}});
|
||||
new FeedbackReceiver
|
||||
{
|
||||
Id = "batman"
|
||||
}
|
||||
});
|
||||
|
||||
tokensService.Setup(i => i.FindAsync(It.IsAny<TokenListFilters>()))
|
||||
.ReturnsAsync(new[]
|
||||
|
@ -310,7 +311,7 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
TimeUsed = null
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Test
|
||||
var controller = new FeedbackReceiversController(feedbackReceiversService.Object, tokensService.Object,
|
||||
feedbacksService.Object, mapper, null, logger.Object);
|
||||
|
@ -318,6 +319,32 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
|
||||
// Assert
|
||||
Assert.IsType<OkResult>(result);
|
||||
feedbacksService.Verify(i => i.AddFeedbackAsync(It.IsAny<Feedback>(),
|
||||
It.IsAny<FeedbackReceiver>()));
|
||||
tokensService.Verify(i => i.MarkTokenAsUsedAsync(It.IsAny<Token>()));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetFeedbacks_Happy(string guid, ListFeedbacksFiltersDto filters)
|
||||
{
|
||||
// Arrange
|
||||
var mapper = TestUtils.GetMapper();
|
||||
var feedbackReceiversService = new Mock<IFeedbackReceiversService>();
|
||||
var tokensService = new Mock<ITokensService>();
|
||||
var feedbacksService = new Mock<IFeedbacksService>();
|
||||
var logger = new Mock<ILogger<FeedbackReceiversController>>();
|
||||
|
||||
// Test
|
||||
var controller = new FeedbackReceiversController(feedbackReceiversService.Object, tokensService.Object,
|
||||
feedbacksService.Object, mapper, null, logger.Object);
|
||||
var result = await controller.GetFeedbacks(guid, filters);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
|
||||
var listFilters = mapper.Map<FeedbacksListFilters>(filters);
|
||||
listFilters.FeedbackReceiverId = guid;
|
||||
feedbacksService.Verify(i => i.GetFeedbacksAsync(listFilters));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -172,5 +172,47 @@ namespace Retroactiune.Tests.Retroactiune.WebAPI.Controllers
|
|||
Assert.IsType<BadRequestObjectResult>(result);
|
||||
tokens.Verify(i => i.FindAsync(It.IsAny<TokenListFilters>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_CheckToken_NotFound()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = TestUtils.GetMapper();
|
||||
var feedbackService = new Mock<IFeedbackReceiversService>();
|
||||
var tokens = new Mock<ITokensService>();
|
||||
var logger = new Mock<ILogger<TokensController>>();
|
||||
tokens.Setup(i => i.FindAsync(It.IsAny<TokenListFilters>()))
|
||||
.ReturnsAsync(new List<Token>());
|
||||
|
||||
// Test
|
||||
var controller = new TokensController(feedbackService.Object, tokens.Object, logger.Object, mapper);
|
||||
var result = await controller.CheckToken("random");
|
||||
|
||||
// Assert
|
||||
var checkResult = (CheckTokenDto) ((ObjectResult) result).Value;
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.False(checkResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_CheckToken_Valid()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = TestUtils.GetMapper();
|
||||
var feedbackService = new Mock<IFeedbackReceiversService>();
|
||||
var tokens = new Mock<ITokensService>();
|
||||
var logger = new Mock<ILogger<TokensController>>();
|
||||
tokens.Setup(i => i.FindAsync(It.IsAny<TokenListFilters>()))
|
||||
.ReturnsAsync(new List<Token> { new Token() });
|
||||
|
||||
// Test
|
||||
var controller = new TokensController(feedbackService.Object, tokens.Object, logger.Object, mapper);
|
||||
var result = await controller.CheckToken("random");
|
||||
|
||||
// Assert
|
||||
var checkResult = (CheckTokenDto) ((ObjectResult) result).Value;
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
Assert.True(checkResult.IsValid);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Retroactiune.Core.Entities;
|
||||
using Retroactiune.Core.Services;
|
||||
using Retroactiune.DataTransferObjects;
|
||||
|
||||
namespace Retroactiune.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation FeedbackReceiversController and Feedbacks related functionality.
|
||||
/// </summary>
|
||||
public partial class FeedbackReceiversController
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Add Feedback to a FeedbackReceiver.
|
||||
/// </summary>
|
||||
/// <param name="feedbackInDto">The feedback dto.</param>
|
||||
/// <response code="200">The feedback has been added.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[HttpPost("feedbacks")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> AddFeedback([FromBody] FeedbackInDto feedbackInDto)
|
||||
{
|
||||
var tokenEnum = await _tokensService.FindAsync(new TokenListFilters
|
||||
{
|
||||
Ids = new[] {feedbackInDto.TokenId}
|
||||
});
|
||||
var tokens = (tokenEnum as Token[] ?? tokenEnum.ToArray());
|
||||
|
||||
|
||||
if (tokens.Length == 0)
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = "Token not found."
|
||||
});
|
||||
}
|
||||
|
||||
var token = tokens[0];
|
||||
|
||||
var receivers = await _feedbackReceiversService.FindAsync(
|
||||
new[] {token.FeedbackReceiverId}, limit: 1
|
||||
);
|
||||
var feedbackReceivers = receivers as FeedbackReceiver[] ?? receivers.ToArray();
|
||||
if (!feedbackReceivers.Any())
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = $"FeedbackReceiver with id {token.FeedbackReceiverId} not found."
|
||||
});
|
||||
}
|
||||
|
||||
if (!token.IsValid(feedbackReceivers[0]))
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = "Token is invalid."
|
||||
});
|
||||
}
|
||||
|
||||
var feedback = _mapper.Map<Feedback>(feedbackInDto);
|
||||
await Task.WhenAll(_tokensService.MarkTokenAsUsedAsync(token),
|
||||
_feedbacksService.AddFeedbackAsync(feedback, feedbackReceivers[0]));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Feedbacks of a FeedbackReceiver. See <see cref="Feedback"/> and <see cref="FeedbackReceiver"/>.
|
||||
/// </summary>
|
||||
/// <param name="guid">The guid of the FeedbackReceiver.</param>
|
||||
/// <param name="filters">Query filters for filtering the response.</param>
|
||||
/// <response code="200">The feedback has been added.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("{guid}/feedbacks")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetFeedbacks(string guid, [FromQuery] ListFeedbacksFiltersDto filters)
|
||||
{
|
||||
var feedbacksListFilters = _mapper.Map<FeedbacksListFilters>(filters);
|
||||
feedbacksListFilters.FeedbackReceiverId = guid;
|
||||
var response = await _feedbacksService.GetFeedbacksAsync(feedbacksListFilters);
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -16,7 +17,7 @@ namespace Retroactiune.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/feedback_receivers")]
|
||||
public class FeedbackReceiversController : ControllerBase
|
||||
public partial class FeedbackReceiversController : ControllerBase
|
||||
{
|
||||
private readonly IOptions<ApiBehaviorOptions> _apiBehaviorOptions;
|
||||
|
||||
|
@ -46,10 +47,12 @@ namespace Retroactiune.Controllers
|
|||
/// <param name="items">The list of FeedbackReceivers</param>
|
||||
/// <returns>A BasicResponse indicating success.</returns>
|
||||
/// <response code="200">Returns an ok message.</response>
|
||||
/// <response code="400">If the items is invalid</response>
|
||||
/// <response code="400">If the items is invalid</response>
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Post([Required] IEnumerable<FeedbackReceiverInDto> items)
|
||||
{
|
||||
var feedbackReceiversDto = items.ToList();
|
||||
|
@ -77,9 +80,11 @@ namespace Retroactiune.Controllers
|
|||
/// <returns>A NoContent result.</returns>
|
||||
/// <response code="204">The delete is submitted.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
[Authorize]
|
||||
[HttpDelete("{guid}")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<NoContentResult> Delete(
|
||||
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
|
||||
string guid)
|
||||
|
@ -96,11 +101,13 @@ namespace Retroactiune.Controllers
|
|||
/// <returns>A Ok result with a <see cref="FeedbackReceiverOutDto"/>.</returns>
|
||||
/// <response code="200">The item returned successfully.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <response code="404">The item was not found.</response>
|
||||
/// <response code="404">The item was not found.</response>
|
||||
[Authorize]
|
||||
[HttpGet("{guid}")]
|
||||
[ProducesResponseType(typeof(FeedbackReceiverOutDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Get(
|
||||
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
|
||||
string guid)
|
||||
|
@ -126,10 +133,12 @@ namespace Retroactiune.Controllers
|
|||
/// <param name="limit">If set, it will limit the results to N items. Allowed range is 1-1000.</param>
|
||||
/// <returns>A Ok result with a list of <see cref="FeedbackReceiverOutDto"/>.</returns>
|
||||
/// <response code="200">The a list is returned.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(IEnumerable<FeedbackReceiverOutDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> List([FromQuery] IEnumerable<string> filter,
|
||||
[RangeAttribute(1, int.MaxValue, ErrorMessage = "offset is out of range, allowed ranges [1-IntMax]"),
|
||||
FromQuery]
|
||||
|
@ -148,8 +157,10 @@ namespace Retroactiune.Controllers
|
|||
/// <response code="400">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> DeleteMany([Required] IEnumerable<string> ids)
|
||||
{
|
||||
try
|
||||
|
@ -168,80 +179,5 @@ namespace Retroactiune.Controllers
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add Feedback to a FeedbackReceiver.
|
||||
/// </summary>
|
||||
/// <param name="feedbackInDto">The feedback dto.</param>
|
||||
/// <response code="200">The feedback has been added.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[HttpPost("feedbacks")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> AddFeedback([FromBody] FeedbackInDto feedbackInDto)
|
||||
{
|
||||
var tokenEnum = await _tokensService.FindAsync(new TokenListFilters
|
||||
{
|
||||
Ids = new[] {feedbackInDto.TokenId}
|
||||
});
|
||||
var tokens = (tokenEnum as Token[] ?? tokenEnum.ToArray());
|
||||
|
||||
|
||||
if (tokens.Length == 0)
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = "Token not found."
|
||||
});
|
||||
}
|
||||
|
||||
var token = tokens[0];
|
||||
|
||||
var receivers = await _feedbackReceiversService.FindAsync(
|
||||
new[] {token.FeedbackReceiverId}, limit: 1
|
||||
);
|
||||
var feedbackReceivers = receivers as FeedbackReceiver[] ?? receivers.ToArray();
|
||||
if (!feedbackReceivers.Any())
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = $"FeedbackReceiver with id {token.FeedbackReceiverId} not found."
|
||||
});
|
||||
}
|
||||
|
||||
if (!token.IsValid(feedbackReceivers[0]))
|
||||
{
|
||||
return BadRequest(new BasicResponse
|
||||
{
|
||||
Message = "Token is invalid."
|
||||
});
|
||||
}
|
||||
|
||||
var feedback = _mapper.Map<Feedback>(feedbackInDto);
|
||||
await Task.WhenAll(_tokensService.MarkTokenAsUsedAsync(token),
|
||||
_feedbacksService.AddFeedbackAsync(feedback, feedbackReceivers[0]));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Feedbacks of a FeedbackReceiver. See <see cref="Feedback"/> and <see cref="FeedbackReceiver"/>.
|
||||
/// </summary>
|
||||
/// <param name="guid">The guid of the FeedbackReceiver.</param>
|
||||
/// <param name="filters">Query filters for filtering the response.</param>
|
||||
/// <response code="200">The feedback has been added.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{guid}/feedbacks")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> GetFeedbacks(string guid, [FromQuery] ListFeedbacksFiltersDto filters)
|
||||
{
|
||||
// TODO: Unit & Integration test.
|
||||
var feedbacksListFilters = _mapper.Map<FeedbacksListFilters>(filters);
|
||||
feedbacksListFilters.FeedbackReceiverId = guid;
|
||||
var response = await _feedbacksService.GetFeedbacksAsync(feedbacksListFilters);
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -38,8 +40,10 @@ namespace Retroactiune.Controllers
|
|||
/// <response code="200">A list of tokens.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(IEnumerable<Token>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ListTokens([FromQuery] ListTokensFiltersDto filtersDto)
|
||||
{
|
||||
try
|
||||
|
@ -67,8 +71,10 @@ namespace Retroactiune.Controllers
|
|||
/// <response code="200">Returns ok.</response>
|
||||
/// <response code="400">If the items is invalid</response>
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GenerateTokens([Required] GenerateTokensDto generateTokensDto)
|
||||
{
|
||||
var feedbackReceiverId = generateTokensDto.FeedbackReceiverId;
|
||||
|
@ -98,8 +104,10 @@ namespace Retroactiune.Controllers
|
|||
/// <response code="404">The request is invalid.</response>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> DeleteTokens([Required] IEnumerable<string> tokenIds)
|
||||
{
|
||||
try
|
||||
|
@ -123,10 +131,12 @@ namespace Retroactiune.Controllers
|
|||
/// <param name="guid">The guid of the item to be deleted.</param>
|
||||
/// <returns>A NoContent result.</returns>
|
||||
/// <response code="204">The delete is submitted.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
[Authorize]
|
||||
[HttpDelete("{guid}")]
|
||||
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType( StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> DeleteToken(
|
||||
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
|
||||
string guid)
|
||||
|
@ -145,5 +155,40 @@ namespace Retroactiune.Controllers
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a token is valid or not.
|
||||
/// </summary>
|
||||
/// <response code="200">The the result of the check.</response>
|
||||
/// <response code="400">The request is invalid.</response>
|
||||
[HttpGet("{guid}/check")]
|
||||
[ProducesResponseType(typeof(CheckTokenDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CheckToken(
|
||||
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
|
||||
string guid
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _tokensService.FindAsync(new TokenListFilters
|
||||
{
|
||||
Ids = new[] {guid}
|
||||
});
|
||||
var token = response.ElementAt(0);
|
||||
return Ok(new CheckTokenDto
|
||||
{
|
||||
IsValid = token.IsValid()
|
||||
});
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
_logger.LogWarning("Invalid token {Guid}", guid);
|
||||
return Ok(new CheckTokenDto
|
||||
{
|
||||
IsValid = false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
Retroactiune.WebAPI/DataTransferObjects/CheckTokenDto.cs
Normal file
7
Retroactiune.WebAPI/DataTransferObjects/CheckTokenDto.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Retroactiune.DataTransferObjects
|
||||
{
|
||||
public class CheckTokenDto
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ namespace Retroactiune.DataTransferObjects
|
|||
/// </summary>
|
||||
public class ListFeedbacksFiltersDto
|
||||
{
|
||||
public uint Rating { get; set; }
|
||||
public uint? Rating { get; set; }
|
||||
public DateTime? CreatedAfter { get; set; }
|
||||
public DateTime? CreatedBefore { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>Retroactiune</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.12.3" />
|
||||
<PackageReference Include="prometheus-net" Version="4.2.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.2.0" />
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Retroactiune.Core\Retroactiune.Core.csproj" />
|
||||
<ProjectReference Include="..\Retroactiune.Infrastructure\Retroactiune.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>Retroactiune</RootNamespace>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
<UserSecretsId>0efb2158-cb38-4e00-9900-48a0fa4ce3c1</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.17" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.12.3" />
|
||||
<PackageReference Include="prometheus-net" Version="4.2.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="4.2.0" />
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Retroactiune.Core\Retroactiune.Core.csproj" />
|
||||
<ProjectReference Include="..\Retroactiune.Infrastructure\Retroactiune.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
@ -8,6 +10,8 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using MongoDB.Driver;
|
||||
using Prometheus;
|
||||
using Retroactiune.Core.Interfaces;
|
||||
|
@ -20,8 +24,6 @@ namespace Retroactiune
|
|||
[ExcludeFromCodeCoverage]
|
||||
public class Startup
|
||||
{
|
||||
// TODO: External auth provider.
|
||||
// TODO: UI?
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
|
@ -51,12 +53,51 @@ namespace Retroactiune
|
|||
return new MongoClient(settings.Value.ConnectionString);
|
||||
});
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = Configuration.GetSection("AuthorizationProvider:Domain").Value,
|
||||
ValidAudience = Configuration.GetSection("AuthorizationProvider:Audience").Value,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(Configuration.GetSection("AuthorizationProvider:SymmetricSecurityKey")
|
||||
.Value))
|
||||
};
|
||||
});
|
||||
|
||||
// WebAPI
|
||||
services.AddControllers();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
var filePath = Path.Combine(AppContext.BaseDirectory, "Retroactiune.WebAPI.xml");
|
||||
c.IncludeXmlComments(filePath);
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Scheme = "bearer",
|
||||
Description = "Please insert JWT token into field"
|
||||
});
|
||||
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
new string[] { }
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -69,7 +110,7 @@ namespace Retroactiune
|
|||
}
|
||||
|
||||
app.UseMetricServer();
|
||||
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
|
@ -78,14 +119,14 @@ namespace Retroactiune
|
|||
});
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
app.UseSentryTracing();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
|
||||
|
||||
logger.LogInformation("Running");
|
||||
}
|
||||
}
|
||||
|
|
33
Retroactiune.WebAPI/TestTokenAuthenticationHandler.cs
Normal file
33
Retroactiune.WebAPI/TestTokenAuthenticationHandler.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Retroactiune
|
||||
{
|
||||
public class TestAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||
public class TestTokenAuthenticationHandler : AuthenticationHandler<TestAuthenticationOptions>
|
||||
{
|
||||
|
||||
public TestTokenAuthenticationHandler(IOptionsMonitor<TestAuthenticationOptions> options, ILoggerFactory logger,
|
||||
UrlEncoder encoder, ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new[] {new Claim("token", "allow_all")};
|
||||
var identity = new ClaimsIdentity(claims, nameof(TestTokenAuthenticationHandler));
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,7 +42,12 @@ namespace Retroactiune
|
|||
var settings = i.GetService<IOptions<DatabaseSettings>>();
|
||||
return new MongoClient(settings.Value.ConnectionString);
|
||||
});
|
||||
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = "Bearer";
|
||||
}).AddScheme<TestAuthenticationOptions, TestTokenAuthenticationHandler> ("Bearer", o => { });
|
||||
|
||||
// WebAPI
|
||||
services.AddControllers();
|
||||
}
|
||||
|
@ -52,6 +57,7 @@ namespace Retroactiune
|
|||
{
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"AuthorizationProvider": {
|
||||
"Domain": "Your AuthProvider Domain",
|
||||
"Audience": "Your API Url",
|
||||
"SymmetricSecurityKey": "HS256 secret"
|
||||
},
|
||||
"Sentry": {
|
||||
"Dsn": "",
|
||||
"MaxRequestBodySize": "Always",
|
||||
|
|
Loading…
Reference in a new issue