Document_Manager #129

Merged
ashutosh.nehete merged 83 commits from Document_Manager into main 2025-09-11 04:12:01 +00:00
2 changed files with 331 additions and 24 deletions
Showing only changes of commit d2af05b6a0 - Show all commits

View File

@ -12,4 +12,15 @@ namespace Marco.Pms.Model.Dtos.DocumentManager
public required FileUploadModel Attachment { get; set; }
public List<DocumentTagDto>? Tags { get; set; }
}
public class UpdateDocumentAttachmentDto
{
public required Guid Id { get; set; }
public required string Name { get; set; }
public string? DocumentId { get; set; }
public required string Description { get; set; }
public FileUploadModel? Attachment { get; set; }
public List<DocumentTagDto>? Tags { get; set; }
}
}

View File

@ -1,9 +1,11 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Filters;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.DocumentManager;
@ -34,6 +36,7 @@ namespace Marco.Pms.Services.Controllers
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
private static readonly string Collection = "DocumentModificationLog";
public DocumentController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScope,
@ -54,7 +57,7 @@ namespace Marco.Pms.Services.Controllers
/// Fetch documents for a given entity (Project/Employee) with filtering, searching, and pagination.
/// </summary>
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
public async Task<IActionResult> GetDocumentList(Guid entityTypeId, Guid entityId, [FromQuery] string? filter, [FromQuery] string? searchString,
public async Task<IActionResult> GetDocumentListAsync(Guid entityTypeId, Guid entityId, [FromQuery] string? filter, [FromQuery] string? searchString,
[FromQuery] bool isActive = true, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
using var scope = _serviceScope.CreateScope();
@ -237,13 +240,12 @@ namespace Marco.Pms.Services.Controllers
/// Validates permissions, document type, entity existence, tags, and uploads to S3.
/// </summary>
[HttpPost("upload")]
public async Task<IActionResult> UploadDocument([FromBody] DocumentAttachmentDto model)
public async Task<IActionResult> UploadDocumentAsync([FromBody] DocumentAttachmentDto model)
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
logger.LogInfo("Document upload initiated for EntityId: {EntityId}, DocumentTypeId: {DocumentTypeId}", model.EntityId, model.DocumentTypeId);
_logger.LogInfo("Document upload initiated for EntityId: {EntityId}, DocumentTypeId: {DocumentTypeId}", model.EntityId, model.DocumentTypeId);
try
{
@ -256,7 +258,7 @@ namespace Marco.Pms.Services.Controllers
if (!hasUploadPermission && loggedInEmployee.Id != model.EntityId)
{
logger.LogWarning("Access Denied. User {UserId} tried to upload document for {EntityId}", loggedInEmployee.Id, model.EntityId);
_logger.LogWarning("Access Denied. User {UserId} tried to upload document for {EntityId}", loggedInEmployee.Id, model.EntityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403));
}
@ -267,14 +269,14 @@ namespace Marco.Pms.Services.Controllers
if (documentType == null)
{
logger.LogWarning("DocumentTypeId {DocumentTypeId} not found for Tenant {TenantId}", model.DocumentTypeId, tenantId);
_logger.LogWarning("DocumentTypeId {DocumentTypeId} not found for Tenant {TenantId}", model.DocumentTypeId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Document Type not found", "Document Type not found in database", 404));
}
// Document ID validation
if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId))
{
logger.LogWarning("Mandatory DocumentId missing for DocumentTypeId: {DocumentTypeId}", documentType.Id);
_logger.LogWarning("Mandatory DocumentId missing for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400));
}
@ -282,7 +284,7 @@ namespace Marco.Pms.Services.Controllers
{
if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression))
{
logger.LogWarning("Invalid DocumentId format for DocumentTypeId: {DocumentTypeId}, Provided: {DocumentId}", documentType.Id, model.DocumentId);
_logger.LogWarning("Invalid DocumentId format for DocumentTypeId: {DocumentTypeId}, Provided: {DocumentId}", documentType.Id, model.DocumentId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400));
}
}
@ -302,7 +304,7 @@ namespace Marco.Pms.Services.Controllers
if (!entityExists)
{
logger.LogWarning("Entity not found. EntityType: {EntityType}, EntityId: {EntityId}", entityType, model.EntityId);
_logger.LogWarning("Entity not found. EntityType: {EntityType}, EntityId: {EntityId}", entityType, model.EntityId);
return NotFound(ApiResponse<object>.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404));
}
@ -310,19 +312,20 @@ namespace Marco.Pms.Services.Controllers
var attachment = _mapper.Map<DocumentAttachment>(model);
attachment.UploadedAt = DateTime.UtcNow;
attachment.UploadedById = loggedInEmployee.Id;
attachment.IsCurrentVersion = true;
attachment.TenantId = tenantId;
// Validate Attachment
if (model.Attachment.FileSize > documentType.MaxSizeAllowedInMB)
{
logger.LogWarning("File size {FileSize} exceeded max allowed {MaxSize}MB", model.Attachment.FileSize, documentType.MaxSizeAllowedInMB);
_logger.LogWarning("File size {FileSize} exceeded max allowed {MaxSize}MB", model.Attachment.FileSize, documentType.MaxSizeAllowedInMB);
return BadRequest(ApiResponse<object>.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400));
}
string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
logger.LogWarning("Missing Base64 data for attachment.");
_logger.LogWarning("Missing Base64 data for attachment.");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data missing", "File data required", 400));
}
@ -332,7 +335,7 @@ namespace Marco.Pms.Services.Controllers
var validContentTypes = documentType.AllowedContentType.Split(',').ToList();
if (!validContentTypes.Contains(fileType))
{
logger.LogWarning("Unsupported file type {FileType} for DocumentType {DocumentTypeId}", fileType, documentType.Id);
_logger.LogWarning("Unsupported file type {FileType} for DocumentType {DocumentTypeId}", fileType, documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400));
}
@ -346,6 +349,7 @@ namespace Marco.Pms.Services.Controllers
// Fire-and-forget upload
_ = Task.Run(async () =>
{
var logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
try
{
await s3Service.UploadFileAsync(base64, fileType, objectKey);
@ -427,7 +431,7 @@ namespace Marco.Pms.Services.Controllers
await dbContext.SaveChangesAsync();
logger.LogInfo("Document uploaded successfully. AttachmentId: {AttachmentId}, DocumentId: {DocumentId}", attachment.Id, document.Id);
_logger.LogInfo("Document uploaded successfully. AttachmentId: {AttachmentId}, DocumentId: {DocumentId}", attachment.Id, document.Id);
var response = _mapper.Map<DocumentListVM>(attachment);
response.UploadedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
@ -438,19 +442,313 @@ namespace Marco.Pms.Services.Controllers
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error during document upload.");
_logger.LogError(ex, "Unexpected error during document upload.");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while uploading the document", 500));
}
}
// PUT api/<DocumentController>/5
[HttpPut("{id}")]
public async Task Put(int id, [FromBody] string value)
[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateDocumentAsync(Guid id, [FromBody] UpdateDocumentAttachmentDto model)
{
// Logger initialization at the start for consistent logging
using var scope = _serviceScope.CreateScope();
_logger.LogInfo("Start UpdateDocument API for AttachmentId: {AttachmentId}", id);
try
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
// Get logged-in employee details
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasUploadPermission = await permissionService.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id);
// Fetch the existing attachment
var oldAttachment = await dbContext.DocumentAttachments
.Include(da => da.DocumentType)
.ThenInclude(dt => dt!.DocumentCategory)
.FirstOrDefaultAsync(da => da.Id == id && da.IsCurrentVersion && da.TenantId == tenantId);
if (oldAttachment == null)
{
_logger.LogWarning("Attachment not found for Id: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Attachment not found", "Attachment not found in database", 404));
}
// Permission check: ensure uploader is authorized
if (!hasUploadPermission && loggedInEmployee.Id != oldAttachment.EntityId)
{
_logger.LogWarning("Access denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403));
}
// Validate the document type
var documentType = oldAttachment.DocumentType;
if (documentType == null)
{
_logger.LogWarning("Document type not found for AttachmentId: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Document Type not found", "Document Type not found in database", 404));
}
// Mandatory DocumentID check
if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId))
{
_logger.LogWarning("Document ID missing for mandatory DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400));
}
// DocumentID Regex validation
if (documentType.IsValidationRequired && !string.IsNullOrWhiteSpace(model.DocumentId) && !string.IsNullOrWhiteSpace(documentType.RegexExpression))
{
if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression))
{
_logger.LogWarning("Provided document ID does not match regex for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400));
}
}
// Validate entity existence and project-level permission
var entityType = documentType.DocumentCategory!.EntityTypeId;
bool entityExists;
if (entityType.Equals(EmployeeEntity))
{
entityExists = await dbContext.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId);
}
else if (entityType.Equals(ProjectEntity))
{
entityExists = await dbContext.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
if (entityExists)
{
entityExists = await permissionService.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
}
}
else
{
entityExists = false;
}
if (!entityExists)
{
_logger.LogWarning("Entity not found (Employee/Project) for AttachmentId: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404));
}
// Prepare for versioning
var oldVersionMapping = await dbContext.AttachmentVersionMappings
.FirstOrDefaultAsync(av => av.ChildAttachmentId == oldAttachment.Id && av.TenantId == tenantId);
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var existingEntityBson = updateLogHelper.EntityToBsonDocument(oldAttachment);
DocumentAttachment newAttachment;
AttachmentVersionMapping newVersionMapping;
if (model.Attachment != null)
{
// File size check
if (model.Attachment.FileSize > documentType.MaxSizeAllowedInMB)
{
_logger.LogWarning("Attachment exceeded max file size for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400));
}
// Base64 validation
string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
_logger.LogWarning("Base64 data missing for new attachment");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data missing", "File data required", 400));
}
// Content type verification
var s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var fileType = s3Service.GetContentTypeFromBase64(base64);
var validContentTypes = documentType.AllowedContentType.Split(',').ToList();
if (!validContentTypes.Contains(fileType))
{
_logger.LogWarning("Unsupported file type: {FileType}", fileType);
return BadRequest(ApiResponse<object>.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400));
}
// S3 keys and folder structure
string folderName = entityType == EmployeeEntity ? "EmployeeDocuments" : "ProjectDocuments";
string fileName = s3Service.GenerateFileName(fileType, tenantId, folderName);
string objectKey = entityType == EmployeeEntity
? $"tenant-{tenantId}/Employee/{oldAttachment.EntityId}/{folderName}/{fileName}"
: $"tenant-{tenantId}/project-{oldAttachment.EntityId}/{folderName}/{fileName}";
// Asynchronous S3 upload with logging
_ = Task.Run(async () =>
{
var logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
try
{
await s3Service.UploadFileAsync(base64, fileType, objectKey);
logger.LogInfo("File uploaded successfully to S3: {ObjectKey}", objectKey);
}
catch (Exception ex)
{
logger.LogError(ex, "S3 upload failed for {ObjectKey}.", objectKey);
}
});
// Create Document record
var document = new Document
{
BatchId = Guid.NewGuid(),
UploadedById = loggedInEmployee.Id,
FileName = model.Attachment.FileName ?? fileName,
ContentType = model.Attachment.ContentType ?? fileType,
S3Key = objectKey,
FileSize = model.Attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
dbContext.Documents.Add(document);
// Record new document attachment as the current version
var attachment = new DocumentAttachment
{
Name = model.Name,
DocumentId = model.DocumentId,
Description = model.Description,
IsCurrentVersion = true,
EntityId = oldAttachment.EntityId,
DocumentDataId = document.Id,
UploadedAt = DateTime.UtcNow,
UploadedById = loggedInEmployee.Id,
DocumentTypeId = oldAttachment.DocumentTypeId,
TenantId = oldAttachment.TenantId
};
dbContext.DocumentAttachments.Add(attachment);
// Mark old version as not current
oldAttachment.IsCurrentVersion = false;
// Version mapping
AttachmentVersionMapping versionMapping;
if (oldVersionMapping != null)
{
versionMapping = new AttachmentVersionMapping
{
ParentAttachmentId = oldVersionMapping.ParentAttachmentId,
ChildAttachmentId = attachment.Id,
Version = (oldVersionMapping.Version + 1),
TenantId = tenantId
};
}
else
{
versionMapping = new AttachmentVersionMapping
{
ParentAttachmentId = attachment.Id,
ChildAttachmentId = attachment.Id,
Version = 1,
TenantId = tenantId
};
}
dbContext.AttachmentVersionMappings.Add(versionMapping);
newAttachment = attachment;
newVersionMapping = versionMapping;
_logger.LogInfo("Created new current version for AttachmentId: {AttachmentId}", attachment.Id);
}
else
{
// Update attachment metadata only (no file upload)
oldAttachment.Name = model.Name;
oldAttachment.DocumentId = model.DocumentId;
oldAttachment.Description = model.Description;
if (oldAttachment.IsVerified == true)
{
oldAttachment.IsVerified = false;
_logger.LogInfo("Reset verification flag for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
oldAttachment.UpdatedAt = DateTime.UtcNow;
oldAttachment.UpdatedById = loggedInEmployee.Id;
newAttachment = oldAttachment;
newVersionMapping = oldVersionMapping ?? new AttachmentVersionMapping();
_logger.LogInfo("Attachment metadata updated for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
// Tag management
if (model.Tags?.Any() == true)
{
var names = model.Tags.Select(t => t.Name).ToList();
var existingTags = await dbContext.DocumentTagMasters
.Where(t => names.Contains(t.Name) && t.TenantId == tenantId)
.ToListAsync();
var attachmentTagMappings = new List<AttachmentTagMapping>();
var oldTagNames = await dbContext.AttachmentTagMappings
.Include(dt => dt.DocumentTag)
.Where(dt => dt.DocumentTag != null && dt.AttachmentId == newAttachment.Id && dt.TenantId == tenantId)
.Select(dt => dt.DocumentTag!.Name)
.ToListAsync();
foreach (var tag in model.Tags.Where(t => t.IsActive && !oldTagNames.Contains(t.Name)))
{
var existingTag = existingTags.FirstOrDefault(t => t.Name == tag.Name);
var tagEntity = existingTag ?? new DocumentTagMaster
{
Id = Guid.NewGuid(),
Name = tag.Name,
Description = tag.Name,
TenantId = tenantId
};
if (existingTag == null)
{
dbContext.DocumentTagMasters.Add(tagEntity);
}
attachmentTagMappings.Add(new AttachmentTagMapping
{
DocumentTagId = tagEntity.Id,
AttachmentId = newAttachment.Id,
TenantId = tenantId
});
}
dbContext.AttachmentTagMappings.AddRange(attachmentTagMappings);
_logger.LogInfo("Tags processed for AttachmentId: {AttachmentId}", newAttachment.Id);
}
// Persist changes to database
await dbContext.SaveChangesAsync();
_logger.LogInfo("Database changes committed for AttachmentId: {AttachmentId}", newAttachment.Id);
// Update logs
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = oldAttachment.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, Collection);
_logger.LogInfo("Update logs pushed for AttachmentId: {AttachmentId}", oldAttachment.Id);
// Prepare response
var response = _mapper.Map<DocumentListVM>(newAttachment);
response.UploadedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.ParentAttachmentId = newVersionMapping.ParentAttachmentId;
response.Version = newVersionMapping.Version;
_logger.LogInfo("API completed successfully for AttachmentId: {AttachmentId}", newAttachment.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Document added successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while updating document for AttachmentId: {AttachmentId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Exception occured", "Exception occured while the updating document", 500));
}
}
// DELETE api/<DocumentController>/5
[HttpDelete("{id}")]
public async Task Delete(int id)
@ -458,11 +756,7 @@ namespace Marco.Pms.Services.Controllers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
}
/// <summary>
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
/// </summary>
/// <param name="filter">The JSON filter string from the request.</param>
/// <returns>An <see cref="TenantFilter"/> object or null if deserialization fails.</returns>
#region =================================================================== Helper Functions ===================================================================
private DocumentFilter? TryDeserializeFilter(string? filter)
{
@ -502,5 +796,7 @@ namespace Marco.Pms.Services.Controllers
}
return documentFilter;
}
#endregion
}
}