Initial import
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MinAttest.Contracts.Employers;
|
||||
using MediatR;
|
||||
using MinAttest.Application.Features.Employers.Commands;
|
||||
using MinAttest.Application.Features.Employers.Queries;
|
||||
using MinAttest.Application.Features.Attests.Commands;
|
||||
using MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
namespace MinAttest.Api.Features.Employer;
|
||||
|
||||
public static class Employers
|
||||
{
|
||||
public static RouteGroupBuilder MapEmployer(this RouteGroupBuilder group)
|
||||
{
|
||||
var employers = group.MapGroup("/employers");
|
||||
|
||||
employers.MapPost("/", UpsertEmployer)
|
||||
.WithName("UpsertEmployer")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/", ListEmployers)
|
||||
.WithName("ListEmployers")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/{id:guid}", GetEmployer)
|
||||
.WithName("GetEmployer")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/{employerId:guid}/users", ListEmployerUsers)
|
||||
.WithName("ListEmployerUsers")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/{employerId:guid}/users/{userId:guid}", GetEmployerUser)
|
||||
.WithName("GetEmployerUser")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapPost("/{employerId:guid}/users", UpsertEmployerUser)
|
||||
.WithName("UpsertEmployerUser")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapDelete("/{employerId:guid}/users/{userId:guid}", DeleteEmployerUser)
|
||||
.WithName("DeleteEmployerUser")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapPost("/{id:guid}/attests", IssueAttestForEmployer)
|
||||
.WithName("IssueAttestForEmployer")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/{id:guid}/attests", ListAttestsForEmployer)
|
||||
.WithName("ListAttestsForEmployer")
|
||||
.WithOpenApi();
|
||||
|
||||
employers.MapGet("/{id:guid}/attests/{attestId:guid}/download", DownloadAttestForEmployer)
|
||||
.WithName("DownloadAttestForEmployer")
|
||||
.WithOpenApi();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> IssueAttestForEmployer(Guid id, EmployerAttestUploadRequest req, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var createdId = await mediator.Send(new EmployerIssueAttestCommand(id, req), ct);
|
||||
if (createdId is null) return Results.NotFound();
|
||||
return Results.Created($"/api/v1/employers/{id}/attests/{createdId}", new { attestId = createdId });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListAttestsForEmployer(Guid id, int? take, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var items = await mediator.Send(new ListEmployerAttestsQuery(id, take), ct);
|
||||
return Results.Ok(items);
|
||||
}
|
||||
|
||||
private static async Task<IResult> DownloadAttestForEmployer(Guid id, Guid attestId, bool? inline, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var content = await mediator.Send(new GetAttestContentQuery(attestId), ct);
|
||||
if (content is null) return Results.NotFound();
|
||||
var contentType = NormalizeContentType(content.Content, content.ContentType);
|
||||
var fileName = NormalizeFileName(content.FileName, attestId, contentType);
|
||||
if (inline == true)
|
||||
{
|
||||
return Results.File(content.Content, contentType);
|
||||
}
|
||||
return Results.File(content.Content, contentType, fileName);
|
||||
}
|
||||
|
||||
private static string NormalizeContentType(byte[] data, string? original)
|
||||
{
|
||||
var ct = string.IsNullOrWhiteSpace(original) || string.Equals(original, "application/octet-stream", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: original;
|
||||
if (ct is null)
|
||||
{
|
||||
if (data is { Length: >= 4 } && data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46)
|
||||
{
|
||||
return "application/pdf";
|
||||
}
|
||||
}
|
||||
return ct ?? "application/pdf";
|
||||
}
|
||||
|
||||
private static string NormalizeFileName(string? name, Guid attestId, string contentType)
|
||||
{
|
||||
var n = string.IsNullOrWhiteSpace(name) ? $"attest-{attestId}" : name!;
|
||||
if (string.Equals(contentType, "application/pdf", StringComparison.OrdinalIgnoreCase) && !n.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
n += ".pdf";
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpsertEmployer(EmployerUpsertRequest req, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new UpsertEmployerCommand(req.OrgNumber, req.Name), ct);
|
||||
return Results.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListEmployers(IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new ListEmployersQuery(), ct);
|
||||
return Results.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<EmployerResponse>, NotFound>> GetEmployer(Guid id, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new GetEmployerQuery(id), ct);
|
||||
if (resp is null) return TypedResults.NotFound();
|
||||
return TypedResults.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListEmployerUsers(Guid employerId, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new ListEmployerUsersQuery(employerId), ct);
|
||||
return Results.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<EmployerUserResponse>, NotFound>> GetEmployerUser(Guid employerId, Guid userId, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new GetEmployerUserQuery(employerId, userId), ct);
|
||||
if (resp is null) return TypedResults.NotFound();
|
||||
return TypedResults.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<EmployerUserResponse>, NotFound, BadRequest<string>>> UpsertEmployerUser(Guid employerId, EmployerUserUpsertRequest req, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new UpsertEmployerUserCommand(employerId, req.ExternalObjectId, req.Email, req.Name, req.Role), ct);
|
||||
if (resp is null) return TypedResults.NotFound();
|
||||
return TypedResults.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<Results<NoContent, NotFound>> DeleteEmployerUser(Guid employerId, Guid userId, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var ok = await mediator.Send(new DeleteEmployerUserCommand(employerId, userId), ct);
|
||||
if (!ok) return TypedResults.NotFound();
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace MinAttest.Api.Features.Health;
|
||||
|
||||
using MinAttest.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public static class Health
|
||||
{
|
||||
public static RouteGroupBuilder MapHealth(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("/health", GetHealth)
|
||||
.WithName("Health")
|
||||
.WithOpenApi();
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetHealth(AppDbContext db, CancellationToken ct)
|
||||
{
|
||||
var canConnect = await db.Database.CanConnectAsync(ct);
|
||||
var payload = new
|
||||
{
|
||||
status = canConnect ? "OK" : "DEGRADED",
|
||||
database = canConnect ? "Up" : "Down",
|
||||
time = DateTimeOffset.UtcNow
|
||||
};
|
||||
return canConnect ? Results.Ok(payload) : Results.Json(payload, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using MinAttest.Contracts.Persons;
|
||||
using MinAttest.Contracts.Attests;
|
||||
using MediatR;
|
||||
using MinAttest.Application.Features.Persons.Commands;
|
||||
using MinAttest.Application.Features.Persons.Queries;
|
||||
using MinAttest.Application.Features.Attests.Commands;
|
||||
using MinAttest.Application.Features.Attests.Queries;
|
||||
|
||||
namespace MinAttest.Api.Features.Persons;
|
||||
|
||||
public static class Persons
|
||||
{
|
||||
public static RouteGroupBuilder MapPersons(this RouteGroupBuilder group)
|
||||
{
|
||||
var persons = group.MapGroup("/persons");
|
||||
|
||||
persons.MapPost("/", UpsertPerson)
|
||||
.WithName("UpsertPerson")
|
||||
.WithOpenApi();
|
||||
|
||||
persons.MapGet("/{id:guid}", GetPerson)
|
||||
.WithName("GetPerson")
|
||||
.WithOpenApi();
|
||||
|
||||
persons.MapPost("/{id:guid}/attests", UploadAttestForPerson)
|
||||
.WithName("UploadAttestForPerson")
|
||||
.WithOpenApi();
|
||||
|
||||
persons.MapGet("/{id:guid}/attests", ListAttestsForPerson)
|
||||
.WithName("ListAttestsForPerson")
|
||||
.WithOpenApi();
|
||||
|
||||
persons.MapGet("/{id:guid}/attests/{attestId:guid}/download", DownloadAttestForPerson)
|
||||
.WithName("DownloadAttestForPerson")
|
||||
.WithOpenApi();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpsertPerson(PersonUpsertRequest req, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new UpsertPersonCommand(req.NationalIdHash, req.Email, req.Phone), ct);
|
||||
return Results.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<PersonResponse>, NotFound>> GetPerson(Guid id, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var resp = await mediator.Send(new GetPersonQuery(id), ct);
|
||||
if (resp is null) return TypedResults.NotFound();
|
||||
return TypedResults.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<IResult> UploadAttestForPerson(Guid id, PersonAttestUploadRequest req, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var createdId = await mediator.Send(new PersonUploadAttestCommand(id, req), ct);
|
||||
if (createdId is null) return Results.NotFound();
|
||||
return Results.Created($"/api/v1/persons/{id}/attests/{createdId}", new { attestId = createdId });
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListAttestsForPerson(Guid id, int? take, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var items = await mediator.Send(new ListPersonAttestsQuery(id, take), ct);
|
||||
return Results.Ok(items);
|
||||
}
|
||||
|
||||
private static async Task<IResult> DownloadAttestForPerson(Guid id, Guid attestId, bool? inline, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var content = await mediator.Send(new GetAttestContentQuery(attestId), ct);
|
||||
if (content is null) return Results.NotFound();
|
||||
var contentType = NormalizeContentType(content.Content, content.ContentType);
|
||||
var fileName = NormalizeFileName(content.FileName, attestId, contentType);
|
||||
if (inline == true)
|
||||
{
|
||||
// Inline view: omit download filename to avoid attachment disposition
|
||||
return Results.File(content.Content, contentType);
|
||||
}
|
||||
return Results.File(content.Content, contentType, fileName);
|
||||
}
|
||||
|
||||
private static string NormalizeContentType(byte[] data, string? original)
|
||||
{
|
||||
var ct = string.IsNullOrWhiteSpace(original) || string.Equals(original, "application/octet-stream", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: original;
|
||||
// Sniff PDF signature: %PDF-
|
||||
if (ct is null)
|
||||
{
|
||||
if (data is { Length: >= 4 } && data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46)
|
||||
{
|
||||
return "application/pdf";
|
||||
}
|
||||
}
|
||||
return ct ?? "application/pdf"; // prefer PDF when unknown
|
||||
}
|
||||
|
||||
private static string NormalizeFileName(string? name, Guid attestId, string contentType)
|
||||
{
|
||||
var n = string.IsNullOrWhiteSpace(name) ? $"attest-{attestId}" : name!;
|
||||
if (string.Equals(contentType, "application/pdf", StringComparison.OrdinalIgnoreCase) && !n.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
n += ".pdf";
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using MediatR;
|
||||
using MinAttest.Application.Features.ShareLinks.Commands;
|
||||
using MinAttest.Application.Features.ShareLinks.Queries;
|
||||
using MinAttest.Contracts.ShareLinks;
|
||||
|
||||
namespace MinAttest.Api.Features.ShareLinks;
|
||||
|
||||
public static class ShareLinks
|
||||
{
|
||||
public static RouteGroupBuilder MapShareLinks(this RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("/attests/{id:guid}/sharelinks", CreateShareLink)
|
||||
.WithName("CreateShareLinks")
|
||||
.WithOpenApi();
|
||||
|
||||
group.MapGet("/attests/{id:guid}/sharelinks", ListShareLinks)
|
||||
.WithName("ListShareLinks")
|
||||
.WithOpenApi();
|
||||
|
||||
group.MapDelete("/attests/{id:guid}/sharelinks/{shareLinkId:guid}", RevokeShareLink)
|
||||
.WithName("RevokeShareLinks")
|
||||
.WithOpenApi();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateShareLink(Guid id, CreateShareLinkRequest req, HttpContext http, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var baseUrl = $"{http.Request.Scheme}://{http.Request.Host}";
|
||||
var resp = await mediator.Send(new CreateShareLinkCommand(id, req, baseUrl), ct);
|
||||
if (resp is null) return Results.NotFound();
|
||||
return Results.Ok(resp);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListShareLinks(Guid id, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var items = await mediator.Send(new ListShareLinksQuery(id), ct);
|
||||
return Results.Ok(items);
|
||||
}
|
||||
|
||||
private static async Task<IResult> RevokeShareLink(Guid id, Guid shareLinkId, IMediator mediator, CancellationToken ct)
|
||||
{
|
||||
var ok = await mediator.Send(new RevokeShareLinkCommand(id, shareLinkId), ct);
|
||||
if (!ok) return Results.NotFound();
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MinAttest.Api</RootNamespace>
|
||||
<AssemblyName>MinAttest.Api</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.*" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.*" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.*" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.*" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.*" />
|
||||
<PackageReference Include="MediatR" Version="12.*" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MinAttest.ServiceDefaults\MinAttest.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\MinAttest.Infrastructure\MinAttest.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\MinAttest.Domain\MinAttest.Domain.csproj" />
|
||||
<ProjectReference Include="..\MinAttest.Application\MinAttest.Application.csproj" />
|
||||
<ProjectReference Include="..\MinAttest.Contracts\MinAttest.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,75 @@
|
||||
@host = http://localhost:5072
|
||||
|
||||
### Health
|
||||
GET {{host}}/api/v1/health
|
||||
|
||||
### Persons
|
||||
POST {{host}}/api/v1/persons
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"nationalIdHash": "hash123",
|
||||
"email": "user@example.com",
|
||||
"phone": "+4712345678"
|
||||
}
|
||||
|
||||
### Employers
|
||||
POST {{host}}/api/v1/employers
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"orgNumber": "123456789",
|
||||
"name": "Bedrift AS"
|
||||
}
|
||||
|
||||
### Attests - list
|
||||
GET {{host}}/api/v1/persons/{{personId}}/attests
|
||||
|
||||
### Person - upload attest (inline content)
|
||||
POST {{host}}/api/v1/persons/{{personId}}/attests
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Utvikler",
|
||||
"from": "2021-01-01",
|
||||
"to": "2023-12-31",
|
||||
"summary": "Arbeidet med .NET",
|
||||
"blobPath": "",
|
||||
"blobHash": null,
|
||||
"contentBase64": "<base64>",
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
|
||||
### Person - download attest
|
||||
GET {{host}}/api/v1/persons/{{personId}}/attests/{{attestId}}/download
|
||||
|
||||
### Employer - upsert employer
|
||||
POST {{host}}/api/v1/employers
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"orgNumber": "123456789",
|
||||
"name": "Bedrift AS"
|
||||
}
|
||||
|
||||
### Employer - issue attest for person
|
||||
POST {{host}}/api/v1/employers/{{employerId}}/attests
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"personId": "{{personId}}",
|
||||
"title": "Konsulent",
|
||||
"from": "2022-01-01",
|
||||
"to": "2024-01-01",
|
||||
"summary": "Prosjektarbeid",
|
||||
"blobPath": "",
|
||||
"blobHash": null,
|
||||
"contentBase64": "<base64>",
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
|
||||
### Employer - list attests
|
||||
GET {{host}}/api/v1/employers/{{employerId}}/attests
|
||||
|
||||
### Employer - download attest
|
||||
GET {{host}}/api/v1/employers/{{employerId}}/attests/{{attestId}}/download
|
||||
@@ -0,0 +1,132 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using MinAttest.Api.Features.Employer;
|
||||
using MinAttest.Api.Features.Health;
|
||||
using MinAttest.Api.Features.ShareLinks;
|
||||
using MinAttest.Infrastructure.Data;
|
||||
using MinAttest.Api.Features.Persons;
|
||||
using MediatR;
|
||||
using MinAttest.Application;
|
||||
using MinAttest.Application.Abstractions;
|
||||
using FluentValidation;
|
||||
using MinAttest.Application.Common.Behaviors;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
Directory.CreateDirectory("logs");
|
||||
|
||||
builder.Host.UseSerilog((ctx, services, cfg) =>
|
||||
{
|
||||
cfg.ReadFrom.Configuration(ctx.Configuration)
|
||||
.Enrich.FromLogContext();
|
||||
});
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "MinAttest API",
|
||||
Version = "v1"
|
||||
});
|
||||
});
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationAssembly).Assembly));
|
||||
builder.Services.AddScoped<IAppDbContext, MinAttest.Infrastructure.Data.AppDbContext>();
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<ApplicationAssembly>();
|
||||
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
||||
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
var cs = builder.Configuration.GetConnectionString("Default")
|
||||
?? builder.Configuration.GetConnectionString("MinAttest")
|
||||
?? builder.Configuration.GetConnectionString("minattest")
|
||||
?? throw new InvalidOperationException("Missing connection string. Provide ConnectionStrings:Default or :MinAttest.");
|
||||
options.UseSqlServer(cs, sql => sql.EnableRetryOnFailure());
|
||||
});
|
||||
|
||||
// Add DB health check to readiness probe
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddDbContextCheck<AppDbContext>(name: "database");
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
{
|
||||
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
if (origins.Length > 0)
|
||||
{
|
||||
policy.WithOrigins(origins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
// Use relative path so it works with PathBase/proxies
|
||||
c.SwaggerEndpoint("../openapi/v1.json", "MinAttest API v1");
|
||||
c.RoutePrefix = "swagger";
|
||||
});
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseCors("Frontend");
|
||||
|
||||
app.UseExceptionHandler(errApp =>
|
||||
{
|
||||
errApp.Run(async context =>
|
||||
{
|
||||
var feature = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
|
||||
var ex = feature?.Error;
|
||||
var status = ex is FluentValidation.ValidationException ? StatusCodes.Status400BadRequest : StatusCodes.Status500InternalServerError;
|
||||
|
||||
var problem = new
|
||||
{
|
||||
type = status == 400 ? "https://datatracker.ietf.org/doc/html/rfc7807#section-3.1" : "about:blank",
|
||||
title = status == 400 ? "One or more validation errors occurred." : "An unexpected error occurred.",
|
||||
status,
|
||||
detail = app.Environment.IsDevelopment() ? ex?.Message : null,
|
||||
traceId = context.TraceIdentifier,
|
||||
errors = ex is FluentValidation.ValidationException vex
|
||||
? vex.Errors.GroupBy(e => e.PropertyName).ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray())
|
||||
: null
|
||||
};
|
||||
|
||||
context.Response.StatusCode = status;
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
await context.Response.WriteAsJsonAsync(problem);
|
||||
});
|
||||
});
|
||||
|
||||
var apiV1 = app.MapGroup("/api/v1");
|
||||
|
||||
apiV1.MapHealth();
|
||||
apiV1.MapEmployer();
|
||||
apiV1.MapPersons();
|
||||
apiV1.MapShareLinks();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var c = db.Database.GetDbConnection();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5072",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7172;http://localhost:5072",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=127.0.0.1,14335;Database=minattest-dev-sql;User Id=sa;Password=Your_password123;TrustServerCertificate=True;"
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.File" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/api-.log",
|
||||
"rollingInterval": "Day",
|
||||
"retainedFileCountLimit": 7,
|
||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user