update swagger implementation

This commit is contained in:
voltsrage
2026-06-26 04:22:47 +08:00
parent 869006e5e7
commit 9777a335c5
8 changed files with 126 additions and 36 deletions
@@ -19,6 +19,7 @@ public class AuthController : ControllerBase
/// Authenticates a user and returns a JWT with role claims.
/// </summary>
[HttpPost("login")]
[AllowAnonymous]
[ProducesResponseType(typeof(ApiResponse<LoginResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Login([FromBody] LoginRequest req)
@@ -32,16 +33,13 @@ public class AuthController : ControllerBase
/// </summary>
[HttpGet("me")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<UserProfileResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Me()
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var user = await _auth.GetCurrentUserAsync(userId);
return Ok(ApiResponse<object>.Ok(new
{
user.Id, user.Username, user.FullName,
role = user.Role.ToDbString()
}));
return Ok(ApiResponse<UserProfileResponse>.Ok(new UserProfileResponse(
user.Id, user.Username, user.FullName, user.Role.ToDbString())));
}
}
@@ -30,30 +30,31 @@ public class DigitizationBatchesController : ControllerBase
/// Uploads a scanned document and creates a new digitization batch.
/// </summary>
[HttpPost]
[Consumes("multipart/form-data")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
[RequestSizeLimit(25 * 1024 * 1024)]
public async Task<IActionResult> Create(
IFormFile file,
[FromForm] CreateBatchRequest req)
public async Task<IActionResult> Create([FromForm] CreateBatchForm form)
{
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"));
if (!_allowedMimeTypes.Contains(file.ContentType))
if (!_allowedMimeTypes.Contains(form.File.ContentType))
return BadRequest(ApiResponse<object>.Fail(400,
"Accepted formats: PDF, JPEG, PNG.", "INVALID_MIME_TYPE"));
var req = form.ToMetadata();
var parsedBatchType = BatchTypeExtensions.FromDbString(req.BatchType.ToUpperInvariant());
var parsedTrack = string.IsNullOrEmpty(req.Track)
? BatchTrack.Backfill
: BatchTrackExtensions.FromDbString(req.Track.ToUpperInvariant());
var actorUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
using var stream = file.OpenReadStream();
using var stream = form.File.OpenReadStream();
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)));
}
@@ -62,6 +63,7 @@ public class DigitizationBatchesController : ControllerBase
/// </summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Get(Guid id)
{
@@ -76,7 +78,8 @@ public class DigitizationBatchesController : ControllerBase
/// Lists batches with optional filters and pagination.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<BatchListResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> List(
[FromQuery] string? status,
[FromQuery] string? batchType,
@@ -90,14 +93,12 @@ public class DigitizationBatchesController : ControllerBase
BatchTrack? parsedTrack = string.IsNullOrEmpty(track) ? null : BatchTrackExtensions.FromDbString(track.ToUpperInvariant());
var result = await _batches.ListAsync(parsedStatus, parsedBatchType, assignedTo, parsedTrack, page, pageSize);
return Ok(ApiResponse<object>.Ok(new
{
items = result.Items,
page = result.Page,
pageSize = result.PageSize,
totalCount = result.TotalCount,
totalPages = result.TotalPages
}));
return Ok(ApiResponse<BatchListResponse>.Ok(new BatchListResponse(
result.Items.Select(b => BatchDetailResponse.FromEntity(b)).ToList(),
result.Page,
result.PageSize,
result.TotalCount,
result.TotalPages)));
}
/// <summary>
@@ -105,6 +106,7 @@ public class DigitizationBatchesController : ControllerBase
/// </summary>
[HttpPatch("{id:guid}/assign")]
[ProducesResponseType(typeof(ApiResponse<BatchDetailResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
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);
}
+9 -11
View File
@@ -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.IdentityModel.Tokens;
using Minio;
@@ -54,28 +52,26 @@ try
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
});
builder.Services.AddVigilCareRecordsSwagger();
var app = builder.Build();
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<ExceptionHandlerMiddleware>();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
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.UseAuthorization();
app.MapControllers();
app.Run();
using (var scope = app.Services.CreateScope())
{
@@ -83,6 +79,8 @@ try
await db.Database.MigrateAsync();
await DataSeeder.SeedAsync(db);
}
app.Run();
}
catch (System.Exception)
{
@@ -4,15 +4,17 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Minio" Version="6.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />