diff --git a/VigilCareRecordsAPI/Controllers/AuthController.cs b/VigilCareRecordsAPI/Controllers/AuthController.cs index 7f863ec..9c8c4e1 100644 --- a/VigilCareRecordsAPI/Controllers/AuthController.cs +++ b/VigilCareRecordsAPI/Controllers/AuthController.cs @@ -19,6 +19,7 @@ public class AuthController : ControllerBase /// Authenticates a user and returns a JWT with role claims. /// [HttpPost("login")] + [AllowAnonymous] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] public async Task Login([FromBody] LoginRequest req) @@ -32,16 +33,13 @@ public class AuthController : ControllerBase /// [HttpGet("me")] [Authorize] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] public async Task Me() { var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); var user = await _auth.GetCurrentUserAsync(userId); - return Ok(ApiResponse.Ok(new - { - user.Id, user.Username, user.FullName, - role = user.Role.ToDbString() - })); + return Ok(ApiResponse.Ok(new UserProfileResponse( + user.Id, user.Username, user.FullName, user.Role.ToDbString()))); } } \ No newline at end of file diff --git a/VigilCareRecordsAPI/Controllers/DigitizationBatchesController.cs b/VigilCareRecordsAPI/Controllers/DigitizationBatchesController.cs index e0cd76e..517b08b 100644 --- a/VigilCareRecordsAPI/Controllers/DigitizationBatchesController.cs +++ b/VigilCareRecordsAPI/Controllers/DigitizationBatchesController.cs @@ -30,30 +30,31 @@ public class DigitizationBatchesController : ControllerBase /// Uploads a scanned document and creates a new digitization batch. /// [HttpPost] + [Consumes("multipart/form-data")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] [RequestSizeLimit(25 * 1024 * 1024)] - public async Task Create( - IFormFile file, - [FromForm] CreateBatchRequest req) + public async Task Create([FromForm] CreateBatchForm form) { - if (file is null || file.Length == 0) + if (form.File is null || form.File.Length == 0) return BadRequest(ApiResponse.Fail(400, "File is required.", "EMPTY_FILE")); - if (!_allowedMimeTypes.Contains(file.ContentType)) + if (!_allowedMimeTypes.Contains(form.File.ContentType)) return BadRequest(ApiResponse.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.Created(BatchDetailResponse.FromEntity(batch))); } @@ -62,6 +63,7 @@ public class DigitizationBatchesController : ControllerBase /// [HttpGet("{id:guid}")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task Get(Guid id) { @@ -76,7 +78,8 @@ public class DigitizationBatchesController : ControllerBase /// Lists batches with optional filters and pagination. /// [HttpGet] - [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] public async Task 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.Ok(new - { - items = result.Items, - page = result.Page, - pageSize = result.PageSize, - totalCount = result.TotalCount, - totalPages = result.TotalPages - })); + return Ok(ApiResponse.Ok(new BatchListResponse( + result.Items.Select(b => BatchDetailResponse.FromEntity(b)).ToList(), + result.Page, + result.PageSize, + result.TotalCount, + result.TotalPages))); } /// @@ -105,6 +106,7 @@ public class DigitizationBatchesController : ControllerBase /// [HttpPatch("{id:guid}/assign")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status409Conflict)] public async Task Assign(Guid id, [FromBody] AssignBatchRequest req) diff --git a/VigilCareRecordsAPI/Infrastructure/OpenApi/SwaggerServiceCollectionExtensions.cs b/VigilCareRecordsAPI/Infrastructure/OpenApi/SwaggerServiceCollectionExtensions.cs new file mode 100644 index 0000000..8777f67 --- /dev/null +++ b/VigilCareRecordsAPI/Infrastructure/OpenApi/SwaggerServiceCollectionExtensions.cs @@ -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() + } + }); + + var xmlPath = Path.Combine( + AppContext.BaseDirectory, + $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); + if (File.Exists(xmlPath)) + options.IncludeXmlComments(xmlPath); + }); + + return services; + } +} diff --git a/VigilCareRecordsAPI/Models/Records/Auth/UserProfileResponse.cs b/VigilCareRecordsAPI/Models/Records/Auth/UserProfileResponse.cs new file mode 100644 index 0000000..b4ae85e --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/Auth/UserProfileResponse.cs @@ -0,0 +1,7 @@ +/// Authenticated user profile returned by GET /api/v1/auth/me. +public record UserProfileResponse( + Guid Id, + string Username, + string FullName, + string Role +); diff --git a/VigilCareRecordsAPI/Models/Records/Batch/BatchListResponse.cs b/VigilCareRecordsAPI/Models/Records/Batch/BatchListResponse.cs new file mode 100644 index 0000000..8d25c0f --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/Batch/BatchListResponse.cs @@ -0,0 +1,8 @@ +/// Paginated batch list returned by GET /api/v1/digitization-batches. +public record BatchListResponse( + IReadOnlyList Items, + int Page, + int PageSize, + int TotalCount, + int TotalPages +); diff --git a/VigilCareRecordsAPI/Models/Records/Batch/CreateBatchForm.cs b/VigilCareRecordsAPI/Models/Records/Batch/CreateBatchForm.cs new file mode 100644 index 0000000..e96472a --- /dev/null +++ b/VigilCareRecordsAPI/Models/Records/Batch/CreateBatchForm.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +/// +/// Multipart form for batch creation. Combines the uploaded scan with batch metadata +/// so Swagger documents a single multipart/form-data request. +/// +public class CreateBatchForm +{ + /// PDF, JPEG, or PNG scan (max 25 MB). + [Required] + public IFormFile File { get; set; } = null!; + + /// Batch type literal, e.g. PATIENT_REGISTRATION or ENCOUNTER_SUMMARY. + [Required] + public string BatchType { get; set; } = null!; + + /// BACKFILL (default) or LIVE_CAPTURE. + public string? Track { get; set; } + + /// Optional patient link used for duplicate-document detection. + public Guid? PatientId { get; set; } + + public CreateBatchRequest ToMetadata() => new(BatchType, Track, PatientId); +} diff --git a/VigilCareRecordsAPI/Program.cs b/VigilCareRecordsAPI/Program.cs index 9d1eed7..771cb83 100644 --- a/VigilCareRecordsAPI/Program.cs +++ b/VigilCareRecordsAPI/Program.cs @@ -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(); app.UseMiddleware(); - // 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) { diff --git a/VigilCareRecordsAPI/VigilCareRecordsAPI.csproj b/VigilCareRecordsAPI/VigilCareRecordsAPI.csproj index 0e39f4e..cf9ce5b 100644 --- a/VigilCareRecordsAPI/VigilCareRecordsAPI.csproj +++ b/VigilCareRecordsAPI/VigilCareRecordsAPI.csproj @@ -4,15 +4,17 @@ net8.0 enable enable + true + $(NoWarn);1591 - - runtime; build; native; contentfiles; analyzers; buildtransitive - all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all