First commit. Include junk from template to remove
This commit is contained in:
66
src/Web/DependencyInjection.cs
Normal file
66
src/Web/DependencyInjection.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Azure.Identity;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using NSwag;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
services.AddScoped<IUser, CurrentUser>();
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHealthChecks()
|
||||
.AddDbContextCheck<ApplicationDbContext>();
|
||||
|
||||
services.AddExceptionHandler<CustomExceptionHandler>();
|
||||
|
||||
services.AddRazorPages();
|
||||
|
||||
// Customise default API behaviour
|
||||
services.Configure<ApiBehaviorOptions>(options =>
|
||||
options.SuppressModelStateInvalidFilter = true);
|
||||
|
||||
services.AddEndpointsApiExplorer();
|
||||
|
||||
services.AddOpenApiDocument((configure, sp) =>
|
||||
{
|
||||
configure.Title = "Hutopy API";
|
||||
|
||||
// Add JWT
|
||||
configure.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}."
|
||||
});
|
||||
|
||||
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, ConfigurationManager configuration)
|
||||
{
|
||||
var keyVaultUri = configuration["KeyVaultUri"];
|
||||
if (!string.IsNullOrWhiteSpace(keyVaultUri))
|
||||
{
|
||||
configuration.AddAzureKeyVault(
|
||||
new Uri(keyVaultUri),
|
||||
new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
52
src/Web/Endpoints/TodoItems.cs
Normal file
52
src/Web/Endpoints/TodoItems.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
|
||||
using Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoItems : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoItemsWithPagination)
|
||||
.MapPost(CreateTodoItem)
|
||||
.MapPut(UpdateTodoItem, "{id}")
|
||||
.MapPut(UpdateTodoItemDetail, "UpdateDetail/{id}")
|
||||
.MapDelete(DeleteTodoItem, "{id}");
|
||||
}
|
||||
|
||||
public Task<PaginatedList<TodoItemBriefDto>> GetTodoItemsWithPagination(ISender sender, [AsParameters] GetTodoItemsWithPaginationQuery query)
|
||||
{
|
||||
return sender.Send(query);
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoItem(ISender sender, CreateTodoItemCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoItem(ISender sender, int id, UpdateTodoItemCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoItemDetail(ISender sender, int id, UpdateTodoItemDetailCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> DeleteTodoItem(ISender sender, int id)
|
||||
{
|
||||
await sender.Send(new DeleteTodoItemCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
42
src/Web/Endpoints/TodoLists.cs
Normal file
42
src/Web/Endpoints/TodoLists.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.DeleteTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
using Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoLists : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoLists)
|
||||
.MapPost(CreateTodoList)
|
||||
.MapPut(UpdateTodoList, "{id}")
|
||||
.MapDelete(DeleteTodoList, "{id}");
|
||||
}
|
||||
|
||||
public Task<TodosVm> GetTodoLists(ISender sender)
|
||||
{
|
||||
return sender.Send(new GetTodosQuery());
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoList(ISender sender, CreateTodoListCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoList(ISender sender, int id, UpdateTodoListCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> DeleteTodoList(ISender sender, int id)
|
||||
{
|
||||
await sender.Send(new DeleteTodoListCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
12
src/Web/Endpoints/Users.cs
Normal file
12
src/Web/Endpoints/Users.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class Users : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.MapIdentityApi<ApplicationUser>();
|
||||
}
|
||||
}
|
||||
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class WeatherForecasts : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetWeatherForecasts);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecasts(ISender sender)
|
||||
{
|
||||
return await sender.Send(new GetWeatherForecastsQuery());
|
||||
}
|
||||
}
|
||||
3
src/Web/GlobalUsings.cs
Normal file
3
src/Web/GlobalUsings.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using Hutopy.Web.Infrastructure;
|
||||
global using MediatR;
|
||||
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Hutopy.Application.Common.Exceptions;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public class CustomExceptionHandler : IExceptionHandler
|
||||
{
|
||||
private readonly Dictionary<Type, Func<HttpContext, Exception, Task>> _exceptionHandlers;
|
||||
|
||||
public CustomExceptionHandler()
|
||||
{
|
||||
// Register known exception types and handlers.
|
||||
_exceptionHandlers = new()
|
||||
{
|
||||
{ typeof(ValidationException), HandleValidationException },
|
||||
{ typeof(NotFoundException), HandleNotFoundException },
|
||||
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
|
||||
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
var exceptionType = exception.GetType();
|
||||
|
||||
if (_exceptionHandlers.ContainsKey(exceptionType))
|
||||
{
|
||||
await _exceptionHandlers[exceptionType].Invoke(httpContext, exception);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleValidationException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (ValidationException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ValidationProblemDetails(exception.Errors)
|
||||
{
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (NotFoundException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails()
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Title = "The specified resource was not found.",
|
||||
Detail = exception.Message
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status401Unauthorized,
|
||||
Title = "Unauthorized",
|
||||
Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status403Forbidden,
|
||||
Title = "Forbidden",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
|
||||
});
|
||||
}
|
||||
}
|
||||
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public abstract class EndpointGroupBase
|
||||
{
|
||||
public abstract void Map(WebApplication app);
|
||||
}
|
||||
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class IEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static IEndpointRouteBuilder MapGet(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapGet(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPost(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPost(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPut(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPut(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapDelete(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapDelete(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class MethodInfoExtensions
|
||||
{
|
||||
public static bool IsAnonymous(this MethodInfo method)
|
||||
{
|
||||
var invalidChars = new[] { '<', '>' };
|
||||
return method.Name.Any(invalidChars.Contains);
|
||||
}
|
||||
|
||||
public static void AnonymousMethod(this IGuardClause guardClause, Delegate input)
|
||||
{
|
||||
if (input.Method.IsAnonymous())
|
||||
throw new ArgumentException("The endpoint name must be specified when using anonymous handlers.");
|
||||
}
|
||||
}
|
||||
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static RouteGroupBuilder MapGroup(this WebApplication app, EndpointGroupBase group)
|
||||
{
|
||||
var groupName = group.GetType().Name;
|
||||
|
||||
return app
|
||||
.MapGroup($"/api/{groupName}")
|
||||
.WithGroupName(groupName)
|
||||
.WithTags(groupName)
|
||||
.WithOpenApi();
|
||||
}
|
||||
|
||||
public static WebApplication MapEndpoints(this WebApplication app)
|
||||
{
|
||||
var endpointGroupType = typeof(EndpointGroupBase);
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var endpointGroupTypes = assembly.GetExportedTypes()
|
||||
.Where(t => t.IsSubclassOf(endpointGroupType));
|
||||
|
||||
foreach (var type in endpointGroupTypes)
|
||||
{
|
||||
if (Activator.CreateInstance(type) is EndpointGroupBase instance)
|
||||
{
|
||||
instance.Map(app);
|
||||
}
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
26
src/Web/Pages/Error.cshtml
Normal file
26
src/Web/Pages/Error.cshtml
Normal file
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
25
src/Web/Pages/Error.cshtml.cs
Normal file
25
src/Web/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Hutopy.Web.Pages;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
36
src/Web/Pages/Shared/_LoginPartial.cshtml
Normal file
36
src/Web/Pages/Shared/_LoginPartial.cshtml
Normal file
@@ -0,0 +1,36 @@
|
||||
@using Hutopy.Infrastructure.Identity
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
|
||||
@{
|
||||
string? returnUrl = null;
|
||||
var query = ViewContext.HttpContext.Request.Query;
|
||||
if (query.ContainsKey("returnUrl"))
|
||||
{
|
||||
returnUrl = query["returnUrl"];
|
||||
}
|
||||
}
|
||||
|
||||
<ul class="navbar-nav">
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity!.Name!</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/">
|
||||
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
3
src/Web/Pages/_ViewImports.cshtml
Normal file
3
src/Web/Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@using Hutopy.Web
|
||||
@namespace Hutopy.Web.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
64
src/Web/Program.cs
Normal file
64
src/Web/Program.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Hutopy.Infrastructure.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", builder =>
|
||||
{
|
||||
builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddKeyVaultIfConfigured(builder.Configuration);
|
||||
|
||||
builder.Services.AddApplicationServices();
|
||||
builder.Services.AddInfrastructureServices(builder.Configuration);
|
||||
builder.Services.AddWebServices();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
await app.InitialiseDatabaseAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHealthChecks("/health");
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseSwaggerUi(settings =>
|
||||
{
|
||||
settings.Path = "/api";
|
||||
settings.DocumentPath = "/api/specification.json";
|
||||
});
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
|
||||
app.MapRazorPages();
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.UseExceptionHandler(options => { });
|
||||
|
||||
app.Map("/", () => Results.Redirect("/api"));
|
||||
|
||||
app.MapEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
27
src/Web/Properties/launchSettings.json
Normal file
27
src/Web/Properties/launchSettings.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:61846",
|
||||
"sslPort": 44312
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Hutopy.Web": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Web/Services/CurrentUser.cs
Normal file
17
src/Web/Services/CurrentUser.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Web.Services;
|
||||
|
||||
public class CurrentUser : IUser
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUser(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public string? Id => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
44
src/Web/Web.csproj
Normal file
44
src/Web/Web.csproj
Normal file
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Web</RootNamespace>
|
||||
<AssemblyName>Hutopy.Web</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
|
||||
<PackageReference Include="NSwag.AspNetCore" />
|
||||
<PackageReference Include="NSwag.MSBuild">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!-- Auto-generated Open API specification and Angular TypeScript clients -->
|
||||
<PropertyGroup>
|
||||
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="NSwag" AfterTargets="PostBuildEvent" Condition=" '$(Configuration)' == 'Debug' And '$(SkipNSwag)' != 'True' ">
|
||||
<Exec ConsoleToMSBuild="true" ContinueOnError="true" WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net80) run config.nswag /variables:Configuration=$(Configuration)">
|
||||
<Output TaskParameter="ExitCode" PropertyName="NSwagExitCode" />
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="NSwagOutput" />
|
||||
</Exec>
|
||||
|
||||
<Message Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' == '0'" Importance="low" />
|
||||
<Error Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' != '0'" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
139
src/Web/Web.http
Normal file
139
src/Web/Web.http
Normal file
@@ -0,0 +1,139 @@
|
||||
# For more info on HTTP files go to https://aka.ms/vs/httpfile
|
||||
@Web_HostAddress = https://localhost:5001
|
||||
|
||||
@Email=administrator@localhost
|
||||
@Password=Administrator1!
|
||||
@BearerToken=<YourToken>
|
||||
|
||||
# POST Users Register
|
||||
POST {{Web_HostAddress}}/api/Users/Register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "{{Email}}",
|
||||
"password": "{{Password}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# POST Users Login
|
||||
POST {{Web_HostAddress}}/api/Users/Login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "{{Email}}",
|
||||
"password": "{{Password}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# POST Users Refresh
|
||||
POST {{Web_HostAddress}}/api/Users/Refresh
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": ""
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# GET WeatherForecast
|
||||
GET {{Web_HostAddress}}/api/WeatherForecasts
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# GET TodoLists
|
||||
GET {{Web_HostAddress}}/api/TodoLists
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# POST TodoLists
|
||||
POST {{Web_HostAddress}}/api/TodoLists
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// CreateTodoListCommand
|
||||
{
|
||||
"Title": "Backlog"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# PUT TodoLists
|
||||
PUT {{Web_HostAddress}}/api/TodoLists/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoListCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"Title": "Product Backlog"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# DELETE TodoLists
|
||||
DELETE {{Web_HostAddress}}/api/TodoLists/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# GET TodoItems
|
||||
@PageNumber = 1
|
||||
@PageSize = 10
|
||||
GET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}}
|
||||
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# POST TodoItems
|
||||
POST {{Web_HostAddress}}/api/TodoItems
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// CreateTodoItemCommand
|
||||
{
|
||||
"ListId": 1,
|
||||
"Title": "Eat a burrito 🌯"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
#PUT TodoItems UpdateItemDetails
|
||||
PUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoItemDetailCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"ListId": 1,
|
||||
"Priority": 3,
|
||||
"Note": "This is a good idea!"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# PUT TodoItems
|
||||
PUT {{Web_HostAddress}}/api/TodoItems/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoItemCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"Title": "Eat a yummy burrito 🌯",
|
||||
"Done": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# DELETE TodoItem
|
||||
DELETE {{Web_HostAddress}}/api/TodoItems/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
10
src/Web/appsettings.Development.json
Normal file
10
src/Web/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.AspNetCore.SpaProxy": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Web/appsettings.json
Normal file
13
src/Web/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost,1433;Database=TestDeux;User Id=sa;Password={DB_PASSWORD};MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
63
src/Web/config.nswag
Normal file
63
src/Web/config.nswag
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"runtime": "Net80",
|
||||
"defaultVariables": null,
|
||||
"documentGenerator": {
|
||||
"aspNetCoreToOpenApi": {
|
||||
"project": "Web.csproj",
|
||||
"msBuildProjectExtensionsPath": null,
|
||||
"configuration": null,
|
||||
"runtime": null,
|
||||
"targetFramework": null,
|
||||
"noBuild": true,
|
||||
"msBuildOutputPath": null,
|
||||
"verbose": false,
|
||||
"workingDirectory": null,
|
||||
"requireParametersWithoutDefault": true,
|
||||
"apiGroupNames": null,
|
||||
"defaultPropertyNameHandling": "CamelCase",
|
||||
"defaultReferenceTypeNullHandling": "Null",
|
||||
"defaultDictionaryValueReferenceTypeNullHandling": "NotNull",
|
||||
"defaultResponseReferenceTypeNullHandling": "NotNull",
|
||||
"generateOriginalParameterNames": true,
|
||||
"defaultEnumHandling": "Integer",
|
||||
"flattenInheritanceHierarchy": false,
|
||||
"generateKnownTypes": true,
|
||||
"generateEnumMappingDescription": false,
|
||||
"generateXmlObjects": false,
|
||||
"generateAbstractProperties": false,
|
||||
"generateAbstractSchemas": true,
|
||||
"ignoreObsoleteProperties": false,
|
||||
"allowReferencesWithProperties": false,
|
||||
"useXmlDocumentation": true,
|
||||
"resolveExternalXmlDocumentation": true,
|
||||
"excludedTypeNames": [],
|
||||
"serviceHost": null,
|
||||
"serviceBasePath": null,
|
||||
"serviceSchemes": [],
|
||||
"infoTitle": "Hutopy API",
|
||||
"infoDescription": null,
|
||||
"infoVersion": "1.0.0",
|
||||
"documentTemplate": null,
|
||||
"documentProcessorTypes": [],
|
||||
"operationProcessorTypes": [],
|
||||
"typeNameGeneratorType": null,
|
||||
"schemaNameGeneratorType": null,
|
||||
"contractResolverType": null,
|
||||
"serializerSettingsType": null,
|
||||
"useDocumentProvider": true,
|
||||
"documentName": "v1",
|
||||
"aspNetCoreEnvironment": null,
|
||||
"createWebHostBuilderMethod": null,
|
||||
"startupType": null,
|
||||
"allowNullableBodyParameters": true,
|
||||
"useHttpAttributeNameAsOperationId": false,
|
||||
"output": "wwwroot/api/specification.json",
|
||||
"outputType": "OpenApi3",
|
||||
"newLineBehavior": "Auto",
|
||||
"assemblyPaths": [],
|
||||
"assemblyConfig": null,
|
||||
"referencePaths": [],
|
||||
"useNuGetCache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1250
src/Web/wwwroot/api/specification.json
Normal file
1250
src/Web/wwwroot/api/specification.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/Web/wwwroot/favicon.ico
Normal file
BIN
src/Web/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Reference in New Issue
Block a user