Skip to content

Commit

Permalink
Merge pull request #36 from featbit/feat/login-by-password
Browse files Browse the repository at this point in the history
✨ Login By Password Api
  • Loading branch information
deleteLater committed Sep 22, 2022
2 parents cc6e0b0 + 22e1564 commit d8a8701
Show file tree
Hide file tree
Showing 47 changed files with 1,139 additions and 8 deletions.
5 changes: 5 additions & 0 deletions modules/back-end/.gitignore
Expand Up @@ -452,3 +452,8 @@ $RECYCLE.BIN/
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

##
## Verified files
##
*.received.*
1 change: 1 addition & 0 deletions modules/back-end/src/Api/Api.csproj
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>

Expand Down
17 changes: 17 additions & 0 deletions modules/back-end/src/Api/Controllers/ApiControllerBase.cs
@@ -0,0 +1,17 @@
namespace Api.Controllers;

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ApiControllerBase : ControllerBase
{
private ISender? _mediator;

protected ISender Mediator
{
get { return _mediator ??= HttpContext.RequestServices.GetRequiredService<ISender>(); }
}

protected static ApiResponse<TData> Ok<TData>(TData data) => ApiResponse<TData>.Ok(data);

protected static ApiResponse<TData> Error<TData>(string errorCode) => ApiResponse<TData>.Error(errorCode);
}
32 changes: 32 additions & 0 deletions modules/back-end/src/Api/Controllers/ApiResponse.cs
@@ -0,0 +1,32 @@
namespace Api.Controllers;

public record ApiResponse<TData>
{
public bool Success { get; set; }

public IEnumerable<string> Errors { get; set; } = Array.Empty<string>();

public TData? Data { get; set; }

public static ApiResponse<TData> Ok(TData? data)
{
return new ApiResponse<TData>
{
Success = true,
Errors = Array.Empty<string>(),
Data = data
};
}

public static ApiResponse<TData> Error(IEnumerable<string> errors)
{
return new ApiResponse<TData>
{
Success = false,
Errors = errors,
Data = default
};
}

public static ApiResponse<TData> Error(string error) => Error(new[] { error });
}
27 changes: 27 additions & 0 deletions modules/back-end/src/Api/Controllers/BasicController.cs
@@ -0,0 +1,27 @@
namespace Api.Controllers;

/// <summary>
/// this controller is intended for testing mvc basic setups (api versioning, consistent api response...)
/// </summary>
[ApiVersion(1.0)]
[ApiVersion(2.0)]
public class BasicController : ApiControllerBase
{
[HttpGet("string"), MapToApiVersion(1.0)]
public ApiResponse<string> GetStringV1()
{
return Ok("v1");
}

[HttpGet("string"), MapToApiVersion(2.0)]
public ApiResponse<string> GetStringV2()
{
return Ok("v2");
}

[HttpGet("exception")]
public ApiResponse<string> ThrowException()
{
throw new Exception("exception message");
}
}
52 changes: 52 additions & 0 deletions modules/back-end/src/Api/Controllers/ConfigSwaggerOptions.cs
@@ -0,0 +1,52 @@
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Api.Controllers;

// taken from https://github.com/dotnet/aspnet-api-versioning/blob/main/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs

/// <summary>
/// Configures the Swagger generation options.
/// </summary>
/// <remarks>This allows API versioning to define a Swagger document per API version after the
/// <see cref="IApiVersionDescriptionProvider"/> service has been resolved from the service container.</remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
/// </summary>
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider;

/// <inheritdoc />
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}

OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "FeatBit Backend Api",
Version = description.ApiVersion.ToString(),
License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
};

if (description.IsDeprecated)
{
info.Description += "<span style=\"color:red\"> This API version has been deprecated.</span>";
}

return info;
}
}
}
18 changes: 18 additions & 0 deletions modules/back-end/src/Api/Controllers/IdentityController.cs
@@ -0,0 +1,18 @@
using Application.Identity;

namespace Api.Controllers;

public class IdentityController : ApiControllerBase
{
[HttpPost]
[AllowAnonymous]
[Route("login-by-password")]
public async Task<ApiResponse<LoginToken>> LoginByPasswordAsync(LoginByPassword request)
{
var loginResult = await Mediator.Send(request);

return loginResult.Success
? Ok(new LoginToken(loginResult.Token))
: Error<LoginToken>(loginResult.ErrorCode);
}
}
@@ -0,0 +1,45 @@
using Api.Controllers;
using Application.Bases;
using Microsoft.AspNetCore.Diagnostics;

namespace Api.Middlewares;

public static class ApiExceptionMiddlewareExtension
{
public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseExceptionHandler(app =>
{
app.Run(async context => await HandleExceptionAsync(context));
});
}

private static async Task HandleExceptionAsync(HttpContext context)
{
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
if (exceptionFeature == null)
{
return;
}

var httpResponse = context.Response;
var ex = exceptionFeature.Error;

// validation exception
if (ex is ValidationException validationException)
{
httpResponse.StatusCode = StatusCodes.Status400BadRequest;

var errors = validationException.Errors.Select(x => x.ErrorCode);
var validationError = ApiResponse<object>.Error(errors);
await httpResponse.WriteAsJsonAsync(validationError);

return;
}

// other exception
httpResponse.StatusCode = StatusCodes.Status500InternalServerError;
var error = ApiResponse<object>.Error(ErrorCodes.InternalServerError);
await httpResponse.WriteAsJsonAsync(error);
}
}
39 changes: 37 additions & 2 deletions modules/back-end/src/Api/Program.cs
@@ -1,19 +1,43 @@
using Api.Controllers;
using Api.Middlewares;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
builder.Services
.AddApiVersioning(options => options.ReportApiVersions = true)
.AddMvc()
.AddApiExplorer(options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();

// health check dependencies
builder.Services.AddHealthChecks();

builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddApplicationServices();

var app = builder.Build();

app.UseApiExceptionHandler();

// reference: https://andrewlock.net/deploying-asp-net-core-applications-to-kubernetes-part-6-adding-health-checks-with-liveness-readiness-and-startup-probes/
// health check endpoints
// external use
Expand All @@ -23,7 +47,18 @@
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwaggerUI(options =>
{
var descriptions = app.DescribeApiVersions();
// build a swagger endpoint for each discovered API version
foreach (var description in descriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
var name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});
}

app.MapControllers();
Expand Down
4 changes: 1 addition & 3 deletions modules/back-end/src/Api/Properties/launchSettings.json
Expand Up @@ -4,9 +4,7 @@
"Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "health/liveness",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"applicationUrl": "https://*:5001;http://*:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
5 changes: 5 additions & 0 deletions modules/back-end/src/Api/Usings.cs
@@ -0,0 +1,5 @@
global using Asp.Versioning;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Authorization;
global using FluentValidation;
global using MediatR;
9 changes: 9 additions & 0 deletions modules/back-end/src/Api/appsettings.json
Expand Up @@ -5,5 +5,14 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Identity": {
"Issuer": "featbit",
"Audience": "featbit-api",
"Key": "featbit-identity-key"
},
"MongoDb": {
"ConnectionString": "mongodb://192.168.1.164:27017",
"Database": "featbit"
},
"AllowedHosts": "*"
}
5 changes: 5 additions & 0 deletions modules/back-end/src/Application/Application.csproj
Expand Up @@ -10,4 +10,9 @@
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
</ItemGroup>

</Project>
@@ -0,0 +1,37 @@
namespace Application.Bases.Behaviours;

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;

public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}

public async Task<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);

var validationResults = _validators.Select(v => v.Validate(context));

var failures = validationResults
.Where(r => r.Errors.Any())
.SelectMany(r => r.Errors)
.ToList();

if (failures.Any())
{
throw new ValidationException(failures);
}
}

return await next();
}
}
13 changes: 13 additions & 0 deletions modules/back-end/src/Application/Bases/ErrorCodes.cs
@@ -0,0 +1,13 @@
namespace Application.Bases;

public static class ErrorCodes
{
// general
public const string InternalServerError = nameof(InternalServerError);

// identity error codes
public const string IdentityIsRequired = nameof(IdentityIsRequired);
public const string PasswordIsRequired = nameof(PasswordIsRequired);
public const string IdentityNotExist = nameof(IdentityNotExist);
public const string IdentityPasswordMismatch = nameof(IdentityPasswordMismatch);
}

0 comments on commit d8a8701

Please sign in to comment.