using System; using System.Net; using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Serilog; using Landmark.OLA.Application.Common.Security; using Landmark.OLA.Application.Configuration; using Landmark.OLA.Application.DataAccess; using Landmark.OLA.Application.DataAccessLayer; using Landmark.OLA.Models.Loan; using Landmark.OLA.Models.Loan.Enums; using OnlineBankingAuth.Tokens; namespace Landmark.OLA.Application.Controllers { [Route("api/[controller]")] [ApiController] public class StorageController : ControllerBase { private readonly ILoanApplicationDataAccess loanApplicationStorage; private readonly ITokenService _tokenService; private readonly BlobStorageSettings _blobStorageSettings; private readonly IBlobStorageAccess _blobStorageAccess; public StorageController( ILoanApplicationDataAccess loanApplicationDataAccess, ITokenService tokenService, IOptions blobStorageSettings, IBlobStorageAccess blobStorageAccess) { loanApplicationStorage = loanApplicationDataAccess; _tokenService = tokenService; _blobStorageSettings = blobStorageSettings.Value; _blobStorageAccess = blobStorageAccess; } /// /// Use to retrieve a set of Loan Containers by filter /// /// /// Returns an array of Loan Containers given a filter /// Cookies: None required or set. /// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role. /// // Post: api/Storage/Filter [HttpPost("Filter")] [ProducesResponseType(typeof(IEnumerable), 200)] [ProducesResponseType(400)] [ProducesResponseType(403)] [ProducesResponseType(500)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] public IActionResult Post([FromBody] Filter filter) { try { return Ok(loanApplicationStorage.GetLoanContainers(filter)); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } /// /// Use to get a specific loan container FOR AN ADMIN /// /// /// Returns a loan container given a loan id. /// Cookies: None required or set. /// Security: Must have a valid JWT token retrieved from Azure AD. /// // GET: api/Storage/5 [HttpGet("{id}", Name = "Get")] [ProducesResponseType(typeof(LoanContainer), 200)] [ProducesResponseType(400)] [ProducesResponseType(403)] [ProducesResponseType(404)] [ProducesResponseType(500)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public IActionResult Get(string id) { try { if (String.IsNullOrEmpty(id)) { return BadRequest("Id is required."); } LoanContainer container = loanApplicationStorage.GetContainerById(id); return Ok(container); } catch (KeyNotFoundException ex) { Log.Error(ex.Message); return NotFound("An application with that Id could not be found"); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } /// /// Use to get loan containers for an authenticated member /// /// /// Returns a list of loan containers given a valid cookie with member id. /// Cookies: Requires authenticated cookie with member id. /// Security: None required or provided. /// // GET: api/Storage [HttpGet("GetMemberApplications", Name = "GetMemberApplications")] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(403)] [ProducesResponseType(500)] public IActionResult GetMemberApplications() { try { var assertion = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); if (string.IsNullOrEmpty(assertion?.MemberID) || string.IsNullOrEmpty(assertion?.IndividualID)) { Log.Error(assertion.MemberID + "/" + assertion.IndividualID, "no stacktrace"); return StatusCode((int)System.Net.HttpStatusCode.Forbidden); } List containers = loanApplicationStorage.GetAllMemberContainers(assertion.MemberID, assertion.IndividualID); return Ok(containers); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)System.Net.HttpStatusCode.InternalServerError); } } /// /// Use to save a loan container /// /// /// Returns a saved loan container with id and possibly member id /// Cookies: Authenticated member cookie optional, will cause id to be saved. /// Security: Currently creates authentication and authorization cookies. /// // POST: api/Storage [HttpPost] [ProducesResponseType(typeof(string), 201)] [ProducesResponseType(400)] [ProducesResponseType(500)] public object Post([FromBody] LoanContainer loanContainer) { try { if (loanContainer != null) { var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); loanContainer.Id = (Guid.NewGuid()).ToString(); loanContainer = MemberIdService.AppendMemberInfo(loanContainer, token); loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); loanApplicationStorage.CreateContainer(loanContainer); HttpContext.Response.StatusCode = 201; return new { id = loanContainer.Id, message = "Successfully saved document" }; } return BadRequest("Loan Application is required."); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpPost("New")] [ProducesResponseType(typeof(string), 201)] [ProducesResponseType(400)] [ProducesResponseType(500)] public object PostNew() { try { var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); var loanContainer = new LoanContainer(new LoanApplication()) { Id = (Guid.NewGuid()).ToString() }; loanContainer = MemberIdService.AppendMemberInfo(loanContainer, token); loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); loanApplicationStorage.CreateContainer(loanContainer); HttpContext.Response.StatusCode = 201; return new { id = loanContainer.Id, message = "Successfully saved document" }; } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } /// /// Use update a loan container /// /// /// Returns a status after attempting to update a loan container with new info. /// Cookies: None required or set. /// Security: None required or provided. /// // PUT: api/Storage [HttpPut] [ProducesResponseType(typeof(string), 200)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(500)] public IActionResult Put([FromBody] LoanContainer loanContainer) { try { var cosmosId = loanContainer?.Id; if (!String.IsNullOrEmpty(cosmosId)) { var assertion = _tokenService.GetAuthenticatedDataFromCookie(HttpContext); loanContainer = CosmosIdService.AppendCosmosId(loanContainer, cosmosId); loanContainer = MemberIdService.AppendMemberInfo(loanContainer, assertion); loanContainer = ForwardedHeaderService.AppendIPAddress(loanContainer, HttpContext); // Append files LoanContainer previousContainer = loanApplicationStorage.GetContainerById(cosmosId); if (previousContainer.LoanApplication != null) { loanContainer.LoanApplication.Files = previousContainer.LoanApplication.Files; } loanApplicationStorage.UpdateContainer(loanContainer); return CreatedAtAction(nameof(Post), new { id = loanContainer.Id }, loanContainer); } return BadRequest("Loan container invalid."); } catch (KeyNotFoundException ex) { Log.Error(ex.Message); return NotFound(); } catch (Exception e) { loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.Error_Updating, e.Message)); Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } /// /// Use to delete a loan /// /// /// Deletes a loan given an id. /// Cookies: None required or set. /// Security: Requires JWT token from Azure AD, AND Admin role. /// // DELETE: api/Storage/Loan/5 [HttpDelete("Loan/{id}")] [ProducesResponseType(200)] [ProducesResponseType(400)] [ProducesResponseType(403)] [ProducesResponseType(404)] [ProducesResponseType(500)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] public IActionResult DeleteLoan(string id) { try { if (String.IsNullOrEmpty(id)) { return BadRequest("Id is required."); } loanApplicationStorage.DeleteApplication(id); loanApplicationStorage.UpdateStatus(id, LoanStatus.Deleted); return Ok(id); } catch (KeyNotFoundException ex) { Log.Error(ex.Message); return NotFound(); } catch (Exception e) { loanApplicationStorage.UpdateError(id, new LoanError(LoanErrorCode.Error_Deleting, e.Message)); Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } /// /// Use to delete an entire loan container /// /// /// Deletes a loan container given a container id. /// Cookies: None required or set. /// Security: Requires JWT token from Azure AD, AND Admin role. /// // DELETE: api/Storage/LoanContainer/5 [HttpDelete("LoanContainer/{id}")] [ProducesResponseType(200)] [ProducesResponseType(400)] [ProducesResponseType(403)] [ProducesResponseType(404)] [ProducesResponseType(500)] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")] public IActionResult DeleteContainer(string id) { try { if (String.IsNullOrEmpty(id)) { return BadRequest("Id is required."); } loanApplicationStorage.DeleteContainer(id); return Ok(id); } catch (KeyNotFoundException ex) { Log.Error(ex.Message); return NotFound(); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } // GET: api/Storage/Settings [HttpGet("Settings")] [ProducesResponseType(typeof(string), 200)] [ProducesResponseType(500)] public IActionResult GetSettings() { try { return Ok(new { fileSizeLimit = _blobStorageSettings.MaxFileSizeInMB, fileLimit = _blobStorageSettings.FileLimit, allowedFileTypes = _blobStorageSettings.AllowedFileTypes }); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } // GET: api/Storage/Documents [HttpGet("Documents/{cosmosLoanId}")] [ProducesResponseType(typeof(string), 200)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(500)] public IActionResult GetDocuments(string cosmosLoanId) { try { if (String.IsNullOrEmpty(cosmosLoanId)) { return BadRequest(new { error = "Invalid or missing loan ID" }); } LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); if(container.LoanApplication == null) { return NotFound(new { loanId = cosmosLoanId }); } return Ok(new { files = container.LoanApplication.Files }); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } // POST: api/Storage/Documents/5 [HttpPost("Documents/{cosmosLoanId}")] [ProducesResponseType(typeof(string), 201)] [ProducesResponseType(400)] [ProducesResponseType(404)] [ProducesResponseType(500)] public IActionResult PostDocument(string cosmosLoanId, [FromForm] DocumentUploadFromLoanApplication request) { try { if (String.IsNullOrEmpty(cosmosLoanId)) { return BadRequest(new { error = "Invalid or missing loan ID" }); } LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); if (container.LoanApplication == null) { return NotFound(new { loanId = cosmosLoanId }); } if (DocumentUploadFromLoanApplication.IsNullOrEmpty(request)) { return BadRequest(new { error = "Cannot upload empty file" }); } using var ms = new MemoryStream(); // Check file type var filename = request.File.FileName.ToLower(); var validExtension = false; foreach (string extension in _blobStorageSettings.AllowedFileTypes) { if (filename.EndsWith(extension)) { validExtension = true; break; } } if (!validExtension) { return BadRequest(new { error = "Invalid file type" }); } // Check file size if (request.File.Length > _blobStorageSettings.MaxFileSizeInMB * 1024 * 1024) { return BadRequest(new { error = $"File size exceeds {_blobStorageSettings.MaxFileSizeInMB} MB" }); } request.File.CopyTo(ms); var fileBytes = ms.ToArray(); var fileId = Guid.NewGuid(); LoanApplicationFile file = new(cosmosLoanId, fileId, request.File.FileName, request.Section, request.SectionId, request.Description); container.LoanApplication.Files.Add(file); loanApplicationStorage.UpdateContainer(container); _blobStorageAccess.SaveBlob(file, fileBytes); return Created($"/api/Storage/Documents/{cosmosLoanId}/{fileId}", new { fileId }); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } // DELETE: api/Storage/Documents/1/5 [HttpDelete("Documents/{cosmosLoanId}/{fileId}")] [ProducesResponseType(typeof(string), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(500)] public IActionResult DeleteDocument(string cosmosLoanId, string fileId) { try { if (String.IsNullOrEmpty(cosmosLoanId) || String.IsNullOrEmpty(fileId)) { return BadRequest(new { error = "Invalid or missing loan or file ID" }); } LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); if (container.LoanApplication == null) { return NotFound(new { loanId = cosmosLoanId }); } foreach (LoanApplicationFile file in container.LoanApplication.Files) { if(file.FileId.ToString().Equals(fileId, StringComparison.OrdinalIgnoreCase)) { _blobStorageAccess.DeleteBlob(file); container.LoanApplication.Files.Remove(file); loanApplicationStorage.UpdateContainer(container); return Ok(new { message = "file deleted", fileId }); } } return NotFound(new { fileId }); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } // DELETE: api/Storage/Documents/1/5 [HttpPost("Documents/DeleteAll/{cosmosLoanId}")] [ProducesResponseType(typeof(string), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(500)] public IActionResult DeleteDocuments(string cosmosLoanId, [FromForm] string files) { try { /*if (String.IsNullOrEmpty(cosmosLoanId) || String.IsNullOrEmpty(fileIds)) { return BadRequest(new { error = "Invalid or missing loan or file ID" }); }*/ LoanContainer container = loanApplicationStorage.GetContainerById(cosmosLoanId); if (container.LoanApplication == null) { return NotFound(new { loanId = cosmosLoanId }); } List filesDeleted = new List(); foreach (LoanApplicationFile file in container.LoanApplication.Files) { if (files.ToString().Contains(file.FileId.ToString())) { _blobStorageAccess.DeleteBlob(file); filesDeleted.Add(file); } } foreach (LoanApplicationFile file in filesDeleted) { container.LoanApplication.Files.Remove(file); } loanApplicationStorage.UpdateContainer(container); return Ok(new { message = "files deleted", filesDeleted }); /*return NotFound(new { fileId });*/ } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } [HttpGet("/GetLog")] [ProducesResponseType(typeof(string), 200)] public IActionResult GetLog() { try { Log.Error("Test log error from localhost"); return Ok("Logging to Seq"); } catch (Exception e) { Log.Error(e.Message); return StatusCode((int)HttpStatusCode.InternalServerError); } } } }