update swagger implementation
This commit is contained in:
@@ -19,6 +19,7 @@ public class AuthController : ControllerBase
|
|||||||
/// Authenticates a user and returns a JWT with role claims.
|
/// Authenticates a user and returns a JWT with role claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(ApiResponse<LoginResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<LoginResponse>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
|
||||||
public async Task<IActionResult> Login([FromBody] LoginRequest req)
|
public async Task<IActionResult> Login([FromBody] LoginRequest req)
|
||||||
@@ -32,16 +33,13 @@ public class AuthController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<UserProfileResponse>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
public async Task<IActionResult> Me()
|
public async Task<IActionResult> Me()
|
||||||
{
|
{
|
||||||
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
var user = await _auth.GetCurrentUserAsync(userId);
|
var user = await _auth.GetCurrentUserAsync(userId);
|
||||||
return Ok(ApiResponse<object>.Ok(new
|
return Ok(ApiResponse<UserProfileResponse>.Ok(new UserProfileResponse(
|
||||||
{
|
user.Id, user.Username, user.FullName, user.Role.ToDbString())));
|
||||||
user.Id, user.Username, user.FullName,
|
|
||||||
role = user.Role.ToDbString()
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,30 +30,31 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
/// Uploads a scanned document and creates a new digitization batch.
|
/// Uploads a scanned document and creates a new digitization batch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status201Created)]
|
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||||
[RequestSizeLimit(25 * 1024 * 1024)]
|
[RequestSizeLimit(25 * 1024 * 1024)]
|
||||||
public async Task<IActionResult> Create(
|
public async Task<IActionResult> Create([FromForm] CreateBatchForm form)
|
||||||
IFormFile file,
|
|
||||||
[FromForm] CreateBatchRequest req)
|
|
||||||
{
|
{
|
||||||
if (file is null || file.Length == 0)
|
if (form.File is null || form.File.Length == 0)
|
||||||
return BadRequest(ApiResponse<object>.Fail(400, "File is required.", "EMPTY_FILE"));
|
return BadRequest(ApiResponse<object>.Fail(400, "File is required.", "EMPTY_FILE"));
|
||||||
|
|
||||||
if (!_allowedMimeTypes.Contains(file.ContentType))
|
if (!_allowedMimeTypes.Contains(form.File.ContentType))
|
||||||
return BadRequest(ApiResponse<object>.Fail(400,
|
return BadRequest(ApiResponse<object>.Fail(400,
|
||||||
"Accepted formats: PDF, JPEG, PNG.", "INVALID_MIME_TYPE"));
|
"Accepted formats: PDF, JPEG, PNG.", "INVALID_MIME_TYPE"));
|
||||||
|
|
||||||
|
var req = form.ToMetadata();
|
||||||
var parsedBatchType = BatchTypeExtensions.FromDbString(req.BatchType.ToUpperInvariant());
|
var parsedBatchType = BatchTypeExtensions.FromDbString(req.BatchType.ToUpperInvariant());
|
||||||
var parsedTrack = string.IsNullOrEmpty(req.Track)
|
var parsedTrack = string.IsNullOrEmpty(req.Track)
|
||||||
? BatchTrack.Backfill
|
? BatchTrack.Backfill
|
||||||
: BatchTrackExtensions.FromDbString(req.Track.ToUpperInvariant());
|
: BatchTrackExtensions.FromDbString(req.Track.ToUpperInvariant());
|
||||||
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
|
||||||
using var stream = file.OpenReadStream();
|
using var stream = form.File.OpenReadStream();
|
||||||
var batch = await _batches.CreateAsync(
|
var batch = await _batches.CreateAsync(
|
||||||
stream, file.ContentType, parsedBatchType, parsedTrack, req.PatientId, actorUserId);
|
stream, form.File.ContentType, parsedBatchType, parsedTrack, req.PatientId, actorUserId);
|
||||||
return StatusCode(201, ApiResponse<BatchDetailResponse>.Created(BatchDetailResponse.FromEntity(batch)));
|
return StatusCode(201, ApiResponse<BatchDetailResponse>.Created(BatchDetailResponse.FromEntity(batch)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
@@ -76,7 +78,8 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
/// Lists batches with optional filters and pagination.
|
/// Lists batches with optional filters and pagination.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchListResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
public async Task<IActionResult> List(
|
public async Task<IActionResult> List(
|
||||||
[FromQuery] string? status,
|
[FromQuery] string? status,
|
||||||
[FromQuery] string? batchType,
|
[FromQuery] string? batchType,
|
||||||
@@ -90,14 +93,12 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
BatchTrack? parsedTrack = string.IsNullOrEmpty(track) ? null : BatchTrackExtensions.FromDbString(track.ToUpperInvariant());
|
BatchTrack? parsedTrack = string.IsNullOrEmpty(track) ? null : BatchTrackExtensions.FromDbString(track.ToUpperInvariant());
|
||||||
|
|
||||||
var result = await _batches.ListAsync(parsedStatus, parsedBatchType, assignedTo, parsedTrack, page, pageSize);
|
var result = await _batches.ListAsync(parsedStatus, parsedBatchType, assignedTo, parsedTrack, page, pageSize);
|
||||||
return Ok(ApiResponse<object>.Ok(new
|
return Ok(ApiResponse<BatchListResponse>.Ok(new BatchListResponse(
|
||||||
{
|
result.Items.Select(b => BatchDetailResponse.FromEntity(b)).ToList(),
|
||||||
items = result.Items,
|
result.Page,
|
||||||
page = result.Page,
|
result.PageSize,
|
||||||
pageSize = result.PageSize,
|
result.TotalCount,
|
||||||
totalCount = result.TotalCount,
|
result.TotalPages)));
|
||||||
totalPages = result.TotalPages
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -105,6 +106,7 @@ public class DigitizationBatchesController : ControllerBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPatch("{id:guid}/assign")]
|
[HttpPatch("{id:guid}/assign")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||||
public async Task<IActionResult> Assign(Guid id, [FromBody] AssignBatchRequest req)
|
public async Task<IActionResult> Assign(Guid id, [FromBody] AssignBatchRequest req)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
public static class SwaggerServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddVigilCareRecordsSwagger(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "VigilCare Records API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "Digitization workflow API for scanned chart intake, verification, and promotion."
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "JWT obtained from POST /api/v1/auth/login"
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Array.Empty<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var xmlPath = Path.Combine(
|
||||||
|
AppContext.BaseDirectory,
|
||||||
|
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
|
||||||
|
if (File.Exists(xmlPath))
|
||||||
|
options.IncludeXmlComments(xmlPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/// <summary>Authenticated user profile returned by GET /api/v1/auth/me.</summary>
|
||||||
|
public record UserProfileResponse(
|
||||||
|
Guid Id,
|
||||||
|
string Username,
|
||||||
|
string FullName,
|
||||||
|
string Role
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/// <summary>Paginated batch list returned by GET /api/v1/digitization-batches.</summary>
|
||||||
|
public record BatchListResponse(
|
||||||
|
IReadOnlyList<BatchDetailResponse> Items,
|
||||||
|
int Page,
|
||||||
|
int PageSize,
|
||||||
|
int TotalCount,
|
||||||
|
int TotalPages
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multipart form for batch creation. Combines the uploaded scan with batch metadata
|
||||||
|
/// so Swagger documents a single <c>multipart/form-data</c> request.
|
||||||
|
/// </summary>
|
||||||
|
public class CreateBatchForm
|
||||||
|
{
|
||||||
|
/// <summary>PDF, JPEG, or PNG scan (max 25 MB).</summary>
|
||||||
|
[Required]
|
||||||
|
public IFormFile File { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>Batch type literal, e.g. PATIENT_REGISTRATION or ENCOUNTER_SUMMARY.</summary>
|
||||||
|
[Required]
|
||||||
|
public string BatchType { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>BACKFILL (default) or LIVE_CAPTURE.</summary>
|
||||||
|
public string? Track { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Optional patient link used for duplicate-document detection.</summary>
|
||||||
|
public Guid? PatientId { get; set; }
|
||||||
|
|
||||||
|
public CreateBatchRequest ToMetadata() => new(BatchType, Track, PatientId);
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Text;using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Minio;
|
using Minio;
|
||||||
@@ -54,28 +52,26 @@ try
|
|||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddVigilCareRecordsSwagger();
|
||||||
{
|
|
||||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
||||||
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
|
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseMiddleware<CorrelationIdMiddleware>();
|
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||||
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Records API v1");
|
||||||
|
options.DocumentTitle = "VigilCare Records API";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.Run();
|
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
@@ -83,6 +79,8 @@ try
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
await DataSeeder.SeedAsync(db);
|
await DataSeeder.SeedAsync(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.Run();
|
||||||
}
|
}
|
||||||
catch (System.Exception)
|
catch (System.Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user