refactor(api): OpenAPI response metadata, cached user sync, ProblemDetails
- Document every endpoint's responses (Produces<T>/status codes/validation) so the generated OpenAPI/Scalar docs carry response schemas and status codes instead of empty responses - Cache recently-synced user ids (IMemoryCache, 15 min) in the JWT TokenValidated handler so user provisioning no longer hits the DB on every authenticated request - Add ProblemDetails + UseExceptionHandler for consistent RFC7807 errors - Add ToCreatedResult ErrorOr helper and use it in the create endpoints - Normalize the playlists route to lowercase (/playlists) Verified: booted the Api against RabbitMQ + Postgres in Docker and inspected /openapi/v1.json — operations now expose response schemas/status codes and the route is /playlists. Build: 0 warnings, 0 errors.
This commit is contained in:
@@ -17,6 +17,7 @@ public static class AuthenticationExtensions
|
|||||||
services.AddSingleton(serviceProvider =>
|
services.AddSingleton(serviceProvider =>
|
||||||
serviceProvider.GetRequiredService<IOptions<AuthenticationConfiguration>>().Value);
|
serviceProvider.GetRequiredService<IOptions<AuthenticationConfiguration>>().Value);
|
||||||
|
|
||||||
|
services.AddMemoryCache();
|
||||||
services.AddScoped<JwtBearerEventsHandler>();
|
services.AddScoped<JwtBearerEventsHandler>();
|
||||||
services.ConfigureOptions<JwtBearerOptionsSetup>();
|
services.ConfigureOptions<JwtBearerOptionsSetup>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Musify.Application.Users;
|
using Musify.Application.Users;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Musify.Api.Authentication;
|
namespace Musify.Api.Authentication;
|
||||||
|
|
||||||
public sealed class JwtBearerEventsHandler(IMediator mediator, ILogger<JwtBearerEventsHandler> logger) : JwtBearerEvents
|
public sealed class JwtBearerEventsHandler(
|
||||||
|
IMediator mediator,
|
||||||
|
IMemoryCache cache,
|
||||||
|
ILogger<JwtBearerEventsHandler> logger) : JwtBearerEvents
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan UserSyncCacheTtl = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
public override async Task TokenValidated(TokenValidatedContext tokenValidatedContext)
|
public override async Task TokenValidated(TokenValidatedContext tokenValidatedContext)
|
||||||
{
|
{
|
||||||
var principal = tokenValidatedContext.Principal!;
|
var principal = tokenValidatedContext.Principal!;
|
||||||
@@ -18,6 +24,10 @@ public sealed class JwtBearerEventsHandler(IMediator mediator, ILogger<JwtBearer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cacheKey = $"user-synced:{userId}";
|
||||||
|
if (cache.TryGetValue(cacheKey, out _))
|
||||||
|
return;
|
||||||
|
|
||||||
var username = principal.FindFirstValue(ClaimTypes.Name)
|
var username = principal.FindFirstValue(ClaimTypes.Name)
|
||||||
?? principal.FindFirstValue("preferred_username")
|
?? principal.FindFirstValue("preferred_username")
|
||||||
?? principal.FindFirstValue(ClaimTypes.Email)
|
?? principal.FindFirstValue(ClaimTypes.Email)
|
||||||
@@ -32,6 +42,8 @@ public sealed class JwtBearerEventsHandler(IMediator mediator, ILogger<JwtBearer
|
|||||||
await mediator.Send(
|
await mediator.Send(
|
||||||
new SyncUserCommand(userId, username, firstName, lastName, profilePictureUrl),
|
new SyncUserCommand(userId, username, firstName, lastName, profilePictureUrl),
|
||||||
tokenValidatedContext.HttpContext.RequestAborted);
|
tokenValidatedContext.HttpContext.RequestAborted);
|
||||||
|
|
||||||
|
cache.Set(cacheKey, true, UserSyncCacheTtl);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Musify.Application.PlayLists;
|
using Musify.Application.PlayLists;
|
||||||
|
using Musify.Application.PlayLists.Responses;
|
||||||
|
using Musify.Application.Tracks.Responses;
|
||||||
|
using Musify.Application.Shared;
|
||||||
using Musify.Api.Authentication;
|
using Musify.Api.Authentication;
|
||||||
using Musify.Api.DataTransferObjects.PlayLists;
|
using Musify.Api.DataTransferObjects.PlayLists;
|
||||||
using Musify.Api.Extensions;
|
using Musify.Api.Extensions;
|
||||||
@@ -11,57 +14,85 @@ public static class PlayListEndpoints
|
|||||||
{
|
{
|
||||||
public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
|
public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
var group = app.MapGroup("/playLists")
|
var group = app.MapGroup("/playlists")
|
||||||
.WithTags("PlayLists");
|
.WithTags("PlayLists");
|
||||||
|
|
||||||
group.MapGet("/", GetPlayLists)
|
group.MapGet("/", GetPlayLists)
|
||||||
.WithName("GetPlayLists")
|
.WithName("GetPlayLists")
|
||||||
.WithSummary("Get Paginated PlayLists.");
|
.WithSummary("Get Paginated PlayLists.")
|
||||||
|
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
|
||||||
|
|
||||||
group.MapGet("/{id}", GetPlayListById)
|
group.MapGet("/{id}", GetPlayListById)
|
||||||
.WithName("GetPlayListById")
|
.WithName("GetPlayListById")
|
||||||
.WithSummary("Get A PlayList By Id.");
|
.WithSummary("Get A PlayList By Id.")
|
||||||
|
.Produces<PlayListApplicationResponse>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapGet("/users/{userId}", GetPlayListsByUserId)
|
group.MapGet("/users/{userId}", GetPlayListsByUserId)
|
||||||
.WithName("GetPlayListsByUserId")
|
.WithName("GetPlayListsByUserId")
|
||||||
.WithSummary("Get Paginated PlayLists For A User.");
|
.WithSummary("Get Paginated PlayLists For A User.")
|
||||||
|
.Produces<PaginatedResponse<PlayListApplicationResponse>>();
|
||||||
|
|
||||||
group.MapGet("/{playlistId}/tracks", GetPlayListTracks)
|
group.MapGet("/{playlistId}/tracks", GetPlayListTracks)
|
||||||
.WithName("GetPlayListTracks")
|
.WithName("GetPlayListTracks")
|
||||||
.WithSummary("Get Paginated Tracks Of A PlayList.");
|
.WithSummary("Get Paginated Tracks Of A PlayList.")
|
||||||
|
.Produces<PaginatedResponse<TrackApplicationResponse>>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapPost("/", CreatePlayList)
|
group.MapPost("/", CreatePlayList)
|
||||||
.WithName("CreatePlayList")
|
.WithName("CreatePlayList")
|
||||||
.WithSummary("Create A New PlayList.")
|
.WithSummary("Create A New PlayList.")
|
||||||
.AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>()
|
.AddEndpointFilter<ValidationFilter<CreatePlayListRequest>>()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<PlayListApplicationResponse>(StatusCodes.Status201Created)
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapPut("/{playlistId}", UpdatePlayList)
|
group.MapPut("/{playlistId}", UpdatePlayList)
|
||||||
.WithName("UpdatePlayList")
|
.WithName("UpdatePlayList")
|
||||||
.WithSummary("Update An Existing PlayList.")
|
.WithSummary("Update An Existing PlayList.")
|
||||||
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
|
.AddEndpointFilter<ValidationFilter<UpdatePlayListRequest>>()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<PlayListApplicationResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapDelete("/{playlistId}", DeletePlayList)
|
group.MapDelete("/{playlistId}", DeletePlayList)
|
||||||
.WithName("DeletePlayList")
|
.WithName("DeletePlayList")
|
||||||
.WithSummary("Delete A PlayList.")
|
.WithSummary("Delete A PlayList.")
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapPost("/upload-picture", RequestPlayListPictureUpload)
|
group.MapPost("/upload-picture", RequestPlayListPictureUpload)
|
||||||
.WithName("RequestPlayListPictureUpload")
|
.WithName("RequestPlayListPictureUpload")
|
||||||
.WithSummary("Request A Pre-Signed URL To Upload A PlayList Picture.")
|
.WithSummary("Request A Pre-Signed URL To Upload A PlayList Picture.")
|
||||||
.AddEndpointFilter<ValidationFilter<RequestPlayListPictureUploadRequest>>()
|
.AddEndpointFilter<ValidationFilter<RequestPlayListPictureUploadRequest>>()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<PlayListPictureUploadResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapPost("/{playlistId}/tracks/{trackId}", AddTrackToPlayList)
|
group.MapPost("/{playlistId}/tracks/{trackId}", AddTrackToPlayList)
|
||||||
.WithName("AddTrackToPlayList")
|
.WithName("AddTrackToPlayList")
|
||||||
.WithSummary("Add A Track To A PlayList.")
|
.WithSummary("Add A Track To A PlayList.")
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
group.MapDelete("/{playlistId}/tracks/{trackId}", RemoveTrackFromPlayList)
|
group.MapDelete("/{playlistId}/tracks/{trackId}", RemoveTrackFromPlayList)
|
||||||
.WithName("RemoveTrackFromPlayList")
|
.WithName("RemoveTrackFromPlayList")
|
||||||
.WithSummary("Remove A Track From A PlayList.")
|
.WithSummary("Remove A Track From A PlayList.")
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -118,10 +149,7 @@ public static class PlayListEndpoints
|
|||||||
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId),
|
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (result.IsError)
|
return result.ToCreatedResult(playList => $"/playlists/{playList.Id}");
|
||||||
return result.ToHttpResult();
|
|
||||||
|
|
||||||
return Results.Created($"/playlists/{result.Value.Id}", result.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> UpdatePlayList(
|
private static async Task<IResult> UpdatePlayList(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Musify.Application.Tracks;
|
using Musify.Application.Tracks;
|
||||||
|
using Musify.Application.Tracks.Responses;
|
||||||
|
using Musify.Application.Shared;
|
||||||
using Musify.Api.Authentication;
|
using Musify.Api.Authentication;
|
||||||
using Musify.Api.DataTransferObjects.Tracks;
|
using Musify.Api.DataTransferObjects.Tracks;
|
||||||
using Musify.Api.Extensions;
|
using Musify.Api.Extensions;
|
||||||
@@ -16,37 +18,58 @@ public static class TrackEndpoints
|
|||||||
|
|
||||||
group.MapGet("/", GetTracks)
|
group.MapGet("/", GetTracks)
|
||||||
.WithName("GetTracks")
|
.WithName("GetTracks")
|
||||||
.WithSummary("Get Paginated Tracks.");
|
.WithSummary("Get Paginated Tracks.")
|
||||||
|
.Produces<PaginatedResponse<TrackApplicationResponse>>();
|
||||||
|
|
||||||
group.MapGet("/{id}", GetTrackById)
|
group.MapGet("/{id}", GetTrackById)
|
||||||
.WithName("GetTrackById")
|
.WithName("GetTrackById")
|
||||||
.WithSummary("Get A Track By Id.");
|
.WithSummary("Get A Track By Id.")
|
||||||
|
.Produces<TrackApplicationResponse>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapGet("/{id}/stream", GetTrackStream)
|
group.MapGet("/{id}/stream", GetTrackStream)
|
||||||
.WithName("GetTrackStream")
|
.WithName("GetTrackStream")
|
||||||
.WithSummary("Get A Streaming Manifest URL And Ticket For A Track.")
|
.WithSummary("Get A Streaming Manifest URL And Ticket For A Track.")
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<TrackStreamResponse>()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapGet("/users/{userId}", GetTracksByUserId)
|
group.MapGet("/users/{userId}", GetTracksByUserId)
|
||||||
.WithName("GetTracksByUserId")
|
.WithName("GetTracksByUserId")
|
||||||
.WithSummary("Get Paginated Tracks For A User.");
|
.WithSummary("Get Paginated Tracks For A User.")
|
||||||
|
.Produces<PaginatedResponse<TrackApplicationResponse>>();
|
||||||
|
|
||||||
group.MapPost("/", CreateTrack)
|
group.MapPost("/", CreateTrack)
|
||||||
.WithName("CreateTrack")
|
.WithName("CreateTrack")
|
||||||
.WithSummary("Create A New Track.")
|
.WithSummary("Create A New Track.")
|
||||||
.AddEndpointFilter<ValidationFilter<CreateTrackRequest>>()
|
.AddEndpointFilter<ValidationFilter<CreateTrackRequest>>()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<TrackApplicationResponse>(StatusCodes.Status201Created)
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
group.MapDelete("/{trackId}", DeleteTrack)
|
group.MapDelete("/{trackId}", DeleteTrack)
|
||||||
.WithName("DeleteTrack")
|
.WithName("DeleteTrack")
|
||||||
.WithSummary("Delete A Track.")
|
.WithSummary("Delete A Track.")
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces(StatusCodes.Status204NoContent)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
group.MapPost("/upload-urls", RequestTrackUploadUrls)
|
group.MapPost("/upload-urls", RequestTrackUploadUrls)
|
||||||
.WithName("RequestTrackUploadUrls")
|
.WithName("RequestTrackUploadUrls")
|
||||||
.WithSummary("Request Pre-Signed URLs To Upload Track Picture And Audio.")
|
.WithSummary("Request Pre-Signed URLs To Upload Track Picture And Audio.")
|
||||||
.AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>()
|
.AddEndpointFilter<ValidationFilter<RequestTrackUploadUrlsRequest>>()
|
||||||
.RequireAuthorization();
|
.RequireAuthorization()
|
||||||
|
.Produces<TrackUploadUrlsResponse>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces(StatusCodes.Status404NotFound)
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -102,10 +125,7 @@ public static class TrackEndpoints
|
|||||||
new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId),
|
new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (result.IsError)
|
return result.ToCreatedResult(track => $"/tracks/{track.Id}");
|
||||||
return result.ToHttpResult();
|
|
||||||
|
|
||||||
return Results.Created($"/tracks/{result.Value.Id}", result.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> DeleteTrack(
|
private static async Task<IResult> DeleteTrack(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Musify.Application.Users;
|
using Musify.Application.Users;
|
||||||
|
using Musify.Application.Users.Responses;
|
||||||
|
using Musify.Application.Shared;
|
||||||
using Musify.Api.DataTransferObjects.Users;
|
using Musify.Api.DataTransferObjects.Users;
|
||||||
using Musify.Api.Extensions;
|
using Musify.Api.Extensions;
|
||||||
using Musify.Api.Filters;
|
using Musify.Api.Filters;
|
||||||
@@ -15,16 +17,22 @@ public static class UserEndpoints
|
|||||||
|
|
||||||
group.MapGet("/", GetUsers)
|
group.MapGet("/", GetUsers)
|
||||||
.WithName("GetUsers")
|
.WithName("GetUsers")
|
||||||
.WithSummary("Get Paginated Users.");
|
.WithSummary("Get Paginated Users.")
|
||||||
|
.Produces<PaginatedResponse<UserApplicationResponse>>();
|
||||||
|
|
||||||
group.MapGet("/{id}", GetUserById)
|
group.MapGet("/{id}", GetUserById)
|
||||||
.WithName("GetUserById")
|
.WithName("GetUserById")
|
||||||
.WithSummary("Get A User By Id.");
|
.WithSummary("Get A User By Id.")
|
||||||
|
.Produces<UserApplicationResponse>()
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
group.MapPost("/", CreateUser)
|
group.MapPost("/", CreateUser)
|
||||||
.WithName("CreateUser")
|
.WithName("CreateUser")
|
||||||
.WithSummary("Create A New User.")
|
.WithSummary("Create A New User.")
|
||||||
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
|
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>()
|
||||||
|
.Produces<UserApplicationResponse>(StatusCodes.Status201Created)
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -58,9 +66,6 @@ public static class UserEndpoints
|
|||||||
new CreateUserCommand(request.Id, request.Name, request.FirstName, request.SecondName),
|
new CreateUserCommand(request.Id, request.Name, request.FirstName, request.SecondName),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
if (result.IsError)
|
return result.ToCreatedResult(user => $"/users/{user.Id}");
|
||||||
return result.ToHttpResult();
|
|
||||||
|
|
||||||
return Results.Created($"/users/{result.Value.Id}", result.Value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ public static class ErrorOrHttpExtensions
|
|||||||
: Results.Ok(result.Value);
|
: Results.Ok(result.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IResult ToCreatedResult<T>(this ErrorOr<T> result, Func<T, string> locationFactory) =>
|
||||||
|
result.IsError
|
||||||
|
? Problem(result.Errors)
|
||||||
|
: Results.Created(locationFactory(result.Value), result.Value);
|
||||||
|
|
||||||
private static IResult Problem(List<Error> errors)
|
private static IResult Problem(List<Error> errors)
|
||||||
{
|
{
|
||||||
if (errors.All(e => e.Type == ErrorType.Validation))
|
if (errors.All(e => e.Type == ErrorType.Validation))
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ builder.Services
|
|||||||
.AddApplicationServices(builder.Configuration)
|
.AddApplicationServices(builder.Configuration)
|
||||||
.AddObservability(builder.Configuration);
|
.AddObservability(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddProblemDetails();
|
||||||
|
|
||||||
if (builder.Environment.IsDevelopment())
|
if (builder.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
builder.Services.AddCors(options => options.AddPolicy(devCorsPolicy, policy =>
|
builder.Services.AddCors(options => options.AddPolicy(devCorsPolicy, policy =>
|
||||||
@@ -19,6 +21,8 @@ if (builder.Environment.IsDevelopment())
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseExceptionHandler();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseCors(devCorsPolicy);
|
app.UseCors(devCorsPolicy);
|
||||||
|
|||||||
Reference in New Issue
Block a user