first commit

This commit is contained in:
Zeeshaun
2026-03-11 11:18:25 -05:00
commit f3cb730f3b
472 changed files with 53651 additions and 0 deletions
@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Serilog;
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Addresses.Validators;
using Landmark.OLA.Models.Vendor;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AddressValidationController : ControllerBase
{
private readonly IUspsVendorClient UspsVendor;
public AddressValidationController(IUspsVendorClient uspsVendor)
{
UspsVendor = uspsVendor;
}
/// <summary>
/// Use to correct a postal address
/// </summary>
/// <remarks>
/// Returns a service corrected postal address, given a postal address.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
[HttpPost]
[ProducesResponseType(typeof(Address), 201)]
[ProducesResponseType(204)]
[ProducesResponseType(typeof(ModelStateDictionary), 400)]
public async Task<IActionResult> Post([FromBody] Address address)
{
try
{
var validator = new AddressValidator();
var validatorResult = validator.Validate(address);
if (!validatorResult.IsValid)
{
foreach (var error in validatorResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
return BadRequest(ModelState);
}
var result = await UspsVendor.CompareAddress(address);
return Ok(result);
}
catch (TimeoutException te)
{
Log.Error(te.Message);
return BadRequest("The time taken to submit the address to the vendor has exceeded the configured timeout length.");
}
catch (ArgumentException )
{
// This happens when the address is valid in form, but not found.
return StatusCode(204);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode(204);
}
}
}
}
@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Serilog;
using Landmark.OLA.Application.DataAccess;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Validators;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BranchConfigController : ControllerBase
{
private readonly IBranchDataAccess branchStorage;
public BranchConfigController(
IBranchDataAccess branchDataAccess)
{
branchStorage = branchDataAccess;
}
/// <summary>
/// Use to get the branch configuration
/// </summary>
/// <remarks>
/// Returns a full branch configuration.
/// Cookies: None required or set.
/// Security: None.
/// </remarks>
// GET: api/BranchConfig
[HttpGet(Name = "GetBranchConfig")]
[ProducesResponseType(typeof(BranchConfig), 200)]
[ProducesResponseType(404)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
BranchConfig config = branchStorage.GetBranchConfig();
return Ok(config);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to get the branch names configuration
/// </summary>
/// <remarks>
/// Returns a full branch configuration.
/// Cookies: None required or set.
/// Security: None.
/// </remarks>
// GET: api/BranchNameConfig
[HttpGet("BranchNameConfig", Name = "GetBranchNameConfig")]
[ProducesResponseType(typeof(BranchNameConfig), 200)]
[ProducesResponseType(404)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public IActionResult GetBranchNameConfig()
{
try
{
BranchNameConfig config = branchStorage.GetBranchNameConfig();
return Ok(config);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to save the initial branch config.
/// </summary>
/// <remarks>
/// Returns a saved branch config with id. This call will fail if ANY configs currently exist.
/// Cookies: None required or set.
/// Security: Requires Azure AD Jwt token and admin role.
/// </remarks>
// POST: api/BranchConfig
[HttpPost]
[ProducesResponseType(typeof(string), 201)]
[ProducesResponseType(403)]
[ProducesResponseType(typeof(ModelStateDictionary), 400)]
[ProducesResponseType(500)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")]
public object Post([FromBody] BranchConfig branchConfig)
{
try
{
if (branchConfig != null)
{
var validator = new BranchConfigValidator();
var validatorResult = validator.Validate(branchConfig);
var catchError = "";
if (!validatorResult.IsValid)
{
foreach (var error in validatorResult.Errors)
{
catchError += error;
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
return BadRequest(ModelState);
}
branchConfig.Id = (Guid.NewGuid()).ToString();
branchConfig.Date = DateTime.Now;
branchStorage.CreateBranchConfig(branchConfig);
HttpContext.Response.StatusCode = 201;
return new {id = branchConfig.Id, message = "Successfully saved document"};
}
return BadRequest("Branch Config is required.");
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use update the branch config
/// </summary>
/// <remarks>
/// Returns a status after attempting to update the branch configuration.
/// Cookies: None required or set.
/// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role.
/// </remarks>
// PUT: api/Storage
[HttpPut]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(typeof(ModelStateDictionary), 400)]
[ProducesResponseType(403)]
[ProducesResponseType(500)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")]
public IActionResult Put([FromBody] BranchConfig branchConfig)
{
try
{
var validator = new BranchConfigValidator();
var validatorResult = validator.Validate(branchConfig);
if (!validatorResult.IsValid)
{
foreach (var error in validatorResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
return BadRequest(ModelState);
}
var cosmosId = branchConfig?.Id;
if (!string.IsNullOrEmpty(cosmosId))
{
branchConfig.Date = DateTime.Now;
branchStorage.UpdateBranchConfig(branchConfig);
return CreatedAtAction(nameof(Put), new {id = branchConfig.Id}, branchConfig);
}
return BadRequest("Branch config invalid.");
}
catch (KeyNotFoundException ex)
{
Log.Error(ex.Message);
return NotFound();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to get an updated branch names configuration
/// </summary>
/// <remarks>
/// Returns a full branch names configuration after updating it internally.
/// Cookies: None required or set.
/// Security: None.
/// </remarks>
// GET: api/UpdateBranchNameConfig
[HttpGet("UpdateBranchNameConfig", Name = "GetUpdateBranchNameConfig")]
[ProducesResponseType(typeof(BranchNameConfig), 200)]
[ProducesResponseType(404)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public IActionResult UpdateBranchNameConfig()
{
try
{
branchStorage.UpdateBranchNameConfig();
BranchNameConfig config = branchStorage.GetBranchNameConfig();
return Ok(config);
}
catch (KeyNotFoundException ex)
{
Log.Error(ex.Message);
return NotFound();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,142 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Serilog;
using Microsoft.AspNetCore.Mvc;
using CRIF.ACTion.Services.Crif;
using Landmark.OLA.Application.Common.Security;
using Landmark.OLA.Application.DataAccessLayer;
using Landmark.OLA.Models.Decisioning;
using Landmark.OLA.Models.Decisioning.Enums;
using Landmark.OLA.Models.Loan.Enums;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/LoanApplication/{loanApplicationId}/DecisioningDetails")]
[ApiController]
public class DecisioningController : ControllerBase
{
private readonly ICrifVendorClient VendorClient;
private readonly ILoanApplicationDataAccess LoanApplicationStorage;
public DecisioningController(
ICrifVendorClient vendorClient,
ILoanApplicationDataAccess loanApplicationDataAccess)
{
VendorClient = vendorClient;
LoanApplicationStorage = loanApplicationDataAccess;
}
/// <summary>
/// Use to start decisioning on a loan
/// </summary>
/// <remarks>
/// Returns status of decision start, on 202 you can begin requesting details.
/// Cookies: Will expire existing cookies.
/// Security: None required or provided.
/// </remarks>
// PUT: api/Application/{loanApplicationId}/DecisioningDetails
[HttpPut]
[ProducesResponseType(202)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task<IActionResult> Put(string loanApplicationId, [FromBody] DecisioningDetails decisioningDetails)
{
if (decisioningDetails.DecisioningState != DecisioningState.Requested)
{
Log.Error("DecisioningState was not set to Requested.");
return BadRequest("Must PUT a DecisioningState of Requested.");
}
Log.Information($"Sending loan {loanApplicationId} to decisioning.");
try
{
var decisioningRequestSuccess = await VendorClient.SendToDecisioning(loanApplicationId);
if (decisioningRequestSuccess)
{
CookieService.ExpireCookies(HttpContext);
return new AcceptedResult();
}
else
{
// SendToDecisioning did not succeed, "That's an error. That's all we know.", Check the server logs!
Log.Error($"Loan #{loanApplicationId} could not be submitted to decisioning.");
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
catch (TimeoutException te)
{
Log.Error(te.Message);
return BadRequest(
"The time taken to submit the loan application to the vendor has exceeded the configured timeout length.");
}
catch (InvalidOperationException ioe)
{
Log.Error(ioe.Message);
return BadRequest(ioe.Message);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to get details of a decision
/// </summary>
/// <remarks>
/// Returns a decision status, which may be empty if the decision is not ready.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// GET: api/Application/{loanApplicationId}/DecisioningDetails
[HttpGet]
[ProducesResponseType(typeof(DecisioningDetails), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get(string loanApplicationId)
{
try
{
var decisioningResult = await VendorClient.GetDecisionStatus(loanApplicationId);
if (decisioningResult.DecisioningState == DecisioningState.Ready)
{
var cosmosId = LoanApplicationStorage.GetContainerIdByLoanId(loanApplicationId);
LoanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Decisioned);
}
return Ok(decisioningResult);
}
catch (TimeoutException te)
{
Log.Error(te.Message);
return BadRequest(
"The time taken to decision the loan application has exceeded the configured timeout length.");
}
catch (ArgumentException ae)
{
Log.Error(ae.Message);
return BadRequest(ae.Message);
}
catch (Error13Exception e13)
{
// We will remove these logs on
Log.Error(e13.Message);
var decisionResult = new DecisioningDetails()
{
DecisioningState = DecisioningState.Unknown,
DecisioningResult = DecisioningResult.ProcessStatus_Incomplete
};
return Ok(decisionResult);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,63 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Landmark.OLA.Application.Services;
using Landmark.OLA.Application.Services.Exceptions;
using Landmark.OLA.Models.Individuals;
using OnlineBankingAuth.Tokens;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IndividualController : ControllerBase
{
private readonly ICoreService _coreService;
private readonly ITokenService _tokenService;
public IndividualController(ITokenService tokenService,
ICoreService coreService)
{
_coreService = coreService;
_tokenService = tokenService;
}
// GET: api/Individual/self
[HttpGet("self", Name = "GetIndividual")]
[ProducesResponseType(typeof(Individual), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(500)]
[ProducesResponseType(503)]
public async Task<IActionResult> Get()
{
try
{
var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext);
if (token?.IndividualID == null )
{
Log.Error("Unauthorized request for 'Self' in Loan API, denying.");
if (token?.MemberID != null)
{
// This isn't logging the security token, but the actual token content.
Log.Error($"Unauthorized request was on behalf of {@token}");
}
return Unauthorized();
}
var individual = await _coreService.GetIndividual(token.MemberID);
return Ok(individual);
}
catch (CoreUnavailableException cue)
{
Log.Error(cue.Message);
return StatusCode((int)HttpStatusCode.ServiceUnavailable);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,440 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
using Serilog;
using CRIF.ACTion.Services.Crif;
using Landmark.OLA.Application.Configuration;
using Landmark.OLA.Application.DataAccess;
using Landmark.OLA.Application.DataAccessLayer;
using Landmark.OLA.Application.Services.MicroApiServices;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Enums;
using Landmark.OLA.Models.Loan.Validators;
using OnlineBankingAuth.Tokens;
using LCU.Library.Internal.NotificationsModels.Models;
using System.Net;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoanApplicationController : ControllerBase
{
private readonly ICrifVendorClient VendorClient;
private readonly ILoanApplicationDataAccess loanApplicationStorage;
private readonly IBranchDataAccess branchStorage;
private readonly IBlobStorageAccess _blobStorageAccess;
private readonly ITokenService _tokenService;
readonly IOptions<DealershipEmailSettings> _dealershipEmailSettings;
private readonly INotificationsApiService _notificationsApiService;
private readonly Services.Email.IEmailTemplateService _emailTemplateService;
public LoanApplicationController(
ICrifVendorClient vendorClient,
ILoanApplicationDataAccess loanApplicationDataAccess,
IBranchDataAccess branchDataAccess,
IBlobStorageAccess blobStorageAccess,
ITokenService tokenService,
IOptions<DealershipEmailSettings> dealershipEmailSettings,
INotificationsApiService notificationsApiService,
IWebHostEnvironment webHostEnvironment,
Services.Email.IEmailTemplateService emailTemplateService)
{
VendorClient = vendorClient;
loanApplicationStorage = loanApplicationDataAccess;
branchStorage = branchDataAccess;
_blobStorageAccess = blobStorageAccess;
_tokenService = tokenService;
_dealershipEmailSettings = dealershipEmailSettings;
_notificationsApiService = notificationsApiService;
_emailTemplateService = emailTemplateService;
}
[HttpPost("AuthenticateDealership")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> AuthenticateDealership([FromBody] DealershipAuthRequest authRequest)
{
try
{
if (string.IsNullOrEmpty(authRequest.ApplicationId))
{
return BadRequest("ApplicationId is required.");
}
authRequest.DealershipName = "";
var authResponse = await VendorClient.GetDealershipAuthDetails(authRequest.ApplicationId);
if (authRequest.Equals(authResponse))
{
// This portion of the try can be uncommented so that the "session" of an upload won't always be re-created.
// There may be a small bug that might only affect app# 10056 if this is re-enabled. Basically the blob file was deleted, but the cosmos record has a reference.
// deleting the cosmos record should resolve the issue
/*try
{
var loanApplicationId = loanApplicationStorage.GetContainerIdByLoanId(authRequest.ApplicationId);
return Ok(new { cosmosId = loanApplicationId });
}
catch (KeyNotFoundException exception)
{*/
var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext);
var loanContainer = new LoanContainer(new LoanApplication())
{
Id = (Guid.NewGuid()).ToString(),
LoanId = authRequest.ApplicationId
};
loanApplicationStorage.CreateContainer(loanContainer);
HttpContext.Response.StatusCode = 201;
return Created($"/api/LoanApplication/AuthenticateDealer/{loanContainer.Id}", new { cosmosId = loanContainer.Id });
/*}*/
}
return Unauthorized();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
[HttpPost("SubmitDealershipFiles/{cosmosId}")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> SubmitDealershipFiles(string cosmosId, [FromBody] DealershipAuthRequest authRequest)
{
try
{
if (string.IsNullOrEmpty(cosmosId))
{
return BadRequest("Cosmos Id is required.");
}
if (string.IsNullOrEmpty(authRequest.ApplicationId))
{
return BadRequest("ApplicationId is required.");
}
authRequest.DealershipName = "";
var authResponse = await VendorClient.GetDealershipAuthDetails(authRequest.ApplicationId);
if (authRequest.Equals(authResponse))
{
LoanContainer loanContainer = loanApplicationStorage.GetContainerById(cosmosId);
LoanApplication loanApplication = loanContainer.LoanApplication;
loanApplication.Id = authResponse.ApplicationId;
// Submit files to ACTion, 1-by-1. Each time the POST request is called, it will upload 1 file. Once the last file has been sent, an email will be sent
if (loanApplication.Files.Count > 0)
{
if (!(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.Equals("Production", StringComparison.OrdinalIgnoreCase) ?? false) &&
loanApplication.Files[0].Name.Equals("do-not-allow-transmission.jpg", StringComparison.OrdinalIgnoreCase))
{
throw new Exception();
}
var fileBase64Value = _blobStorageAccess.GetBlobAsBase64(loanApplication.Files[0]);
var fileSubmitted = await VendorClient.AddFile(loanApplication.Id, loanApplication.Files[0], fileBase64Value);
if (fileSubmitted)
{
_blobStorageAccess.DeleteBlob(loanApplication.Files[0]);
loanApplication.Files.RemoveAt(0);
loanApplicationStorage.UpdateContainer(loanContainer);
// Return the files if there are still some left to upload. If not, send the email and return
if (loanApplication.Files.Count > 0)
{
return StatusCode(201, new { files = loanApplication.Files });
}
}
else
{
Log.Error($"File not sent to ACTion, not deleting file: {loanApplication.Files[0]}");
}
}
if (_dealershipEmailSettings.Value.Recipients != null && _dealershipEmailSettings.Value.Recipients.Count > 0 && !_dealershipEmailSettings.Value.Recipients.Contains("PERENVIRONMENT") && !string.IsNullOrEmpty(_dealershipEmailSettings.Value.ActionLink) && !string.IsNullOrEmpty(_dealershipEmailSettings.Value.EmailAPIUrl))
{
try
{
var redBannerMessage = _dealershipEmailSettings.Value.RedBannerMessage ?? string.Empty;
var contentHtml =
"This is an auto generated message. <br><br>" +
"A new deal has been submitted and is ready to be viewed and exported <br><br>" +
$"Dealership Name: <strong>{authResponse.DealershipName}</strong><br>" +
$"Application ID: <strong>{authResponse.ApplicationId}</strong><br>" +
$"Primary Applicant's Last Name or Business Full Name: <strong>{authResponse.ApplicantLastName}</strong><br><br>" +
$"Click on the following link to open the application record within ACTion: <a href=\"{_dealershipEmailSettings.Value.ActionLink}{authResponse.ApplicationId}\">{authResponse.ApplicationId}</a><br>";
var requestBody = _emailTemplateService.BuildBrandedBody(contentHtml, redBannerMessage);
var request = new EmailRequest()
{
Subject = $"Dealership has uploaded documents to application {loanApplication.Id}",
Recipient = _dealershipEmailSettings.Value.Recipients,
Body = requestBody
};
Log.Information($"Sending email to {_dealershipEmailSettings.Value.Recipients} for application ID: {loanApplication.Id}");
await _notificationsApiService.SendEmailAsync(request);
}
catch (Exception ex)
{
Log.Error($"Error when sending email to loan team for application ID {loanApplication.Id}");
Log.Error(ex.Message);
}
}
loanApplicationStorage.DeleteApplication(cosmosId);
return StatusCode(200, new { files = loanApplication.Files });
}
return Unauthorized();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to transmit an existing loan application to CRIF Lending
/// </summary>
/// <remarks>
/// Transmits a loan application to CRIF lending given a loan storage id (not a loan id).
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// Post: api/LoanApplication/Submit/:id
[HttpPost("Submit/{cosmosId}")]
[ProducesResponseType(typeof(LoanApplication), 201)]
[ProducesResponseType(typeof(ModelStateDictionary), 400)]
[ProducesResponseType(404)]
[ProducesResponseType(520)]
[ProducesResponseType(500)]
public async Task<IActionResult> Post(string cosmosId)
{
try
{
if (string.IsNullOrEmpty(cosmosId))
{
return BadRequest("Id is required.");
}
LoanContainer loanContainer = loanApplicationStorage.GetContainerById(cosmosId);
if ( (loanContainer.Status != LoanStatus.In_Progress && loanContainer.Status != LoanStatus.Vendor_Error
&& loanContainer.Status != LoanStatus.Application_Error) || loanContainer.LoanApplication == null)
{
return BadRequest("Loan is not in progress. Unable to submit.");
}
LoanApplication loanApplication = loanContainer.LoanApplication;
var memberInfo = _tokenService.GetAuthenticatedDataFromCookie(HttpContext);
if (memberInfo != null)
{
loanApplication.PrimaryBorrower.CustomerNumber = memberInfo.MemberID;
}
var validator = new LoanApplicationValidator();
var validatorResult = validator.Validate(loanApplication);
var catchError = "";
if (!validatorResult.IsValid)
{
foreach (var error in validatorResult.Errors)
{
catchError += error;
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.Error_Submitting, catchError));
loanApplicationStorage.UpdateStatus(loanContainer.Id, LoanStatus.Application_Error);
Log.Error($"Could not submit application, errors follow: {@ModelState}");
return BadRequest(ModelState);
}
// CRIF id will be assigned
loanApplication.Id = await VendorClient.CreateApplicationAndGetId(loanApplication, branchStorage);
// Submit files to ACTion
foreach(var file in loanApplication.Files)
{
var fileBase64Value = _blobStorageAccess.GetBlobAsBase64(file);
var fileSubmitted = true;
bool shouldUploadFileToAction = false;
var vehicleTypes = new List<LoanType> { LoanType.V_Vehicle, LoanType.V_VehiclePreapproval, LoanType.P_Powersport, LoanType.T_TrailerMotorhome, LoanType.M_Boat };
if (vehicleTypes.Contains((LoanType)loanApplication.LoanType))
{
if (file.Description.Equals("DNR Registration Card", StringComparison.OrdinalIgnoreCase))
{
shouldUploadFileToAction = true;
}
else
{
shouldUploadFileToAction = loanApplication.Purpose == LoanPurpose.Purpose.Purchase;
}
} else if (loanApplication.LoanType != LoanType.EQ_HELOC_Flex)
{
shouldUploadFileToAction = true;
} else if (loanApplication.LoanType == LoanType.EQ_HELOC_Flex && loanApplication.RequestedAmount > 100000)
{
shouldUploadFileToAction = true;
} else if (!file.Description.Equals("Proof of Income", StringComparison.OrdinalIgnoreCase))
{
shouldUploadFileToAction = true;
}
if (shouldUploadFileToAction)
{
fileSubmitted = await VendorClient.AddFile(loanApplication.Id, file, fileBase64Value);
}
if(fileSubmitted)
{
_blobStorageAccess.DeleteBlob(file);
} else
{
Log.Error($"File not sent to ACTion, not deleting file: {file}");
}
}
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Submitted);
loanApplicationStorage.UpdateError(loanContainer.Id, new LoanError(LoanErrorCode.No_Error, "No_Error"));
loanApplicationStorage.UpdateLoanId(loanContainer.Id, loanApplication.Id);
loanApplicationStorage.DeleteApplication(cosmosId);
return Ok(new { loanApplicationId = loanApplication.Id });
}
catch (KeyNotFoundException ex)
{
Log.Error(ex.Message);
return NotFound("An application with that Id could not be found");
}
catch (InvalidDataException ie)
{
loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, ie.Message));
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Application_Error);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
catch (TimeoutException te)
{
loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "The time taken to submit the loan application to the vendor has exceded the configured timeout length."));
Log.Error(te.Message);
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error);
return BadRequest("The time taken to submit the loan application to the vendor has exceeded the configured timeout length.");
}
catch (ArgumentException ae)
{
loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Invalid argument."));
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error);
Log.Error(ae.Message);
return BadRequest(ae.Message);
}
catch (FormatException fe)
{
loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Vendor returned unexpected response."));
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error);
Log.Error(fe.Message);
return StatusCode(520, fe.Message);
//Cloudflare uses an HTTP 520 when the external "server returns something unexpected"[1]
//We return this when we don't understand CRIF's response
//[1] https://support.cloudflare.com/hc/en-us/articles/206973867-Error-analytics-by-colo
}
catch (Exception e)
{
loanApplicationStorage.UpdateError(cosmosId, new LoanError(LoanErrorCode.Error_Submitting, "Unexpected exception when submitting application."));
loanApplicationStorage.UpdateStatus(cosmosId, LoanStatus.Vendor_Error);
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to transmit a new loan application directly to CRIF Lending
/// </summary>
/// <remarks>
/// Transmits a loan application directly to CRIF Lending given a valid loan application.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// Post: api/LoanApplication/
[HttpPost]
[ProducesResponseType(typeof(LoanApplication), 201)]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)]
[ProducesResponseType(520)]
[ProducesResponseType(500)]
public async Task<IActionResult> Post([FromBody] LoanApplication loanApplication)
{
try
{
var validator = new LoanApplicationValidator();
var validatorResult = validator.Validate(loanApplication);
if (!validatorResult.IsValid)
{
foreach (var error in validatorResult.Errors)
{
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
Log.Error($"Could not submit application, [Possibly third party], errors follow: {@ModelState}");
return BadRequest(ModelState);
}
var application = await VendorClient.CreateApplication(loanApplication, branchStorage);
return CreatedAtRoute("GetApplication", new { loanApplicationId = application.Id }, application);
}
catch (TimeoutException te)
{
Log.Error(te.Message);
return BadRequest("The time taken to submit the loan application to the vendor has exceeded the configured timeout length.");
}
catch (ArgumentException ae)
{
Log.Error(ae.Message);
return BadRequest(ae.Message);
}
catch (FormatException fe)
{
Log.Error(fe.Message);
return StatusCode(520, fe.Message);
//Cloudflare uses an HTTP 520 when the external "server returns something unexpected"[1]
//We return this when we don't understand CRIF's response
//[1] https://support.cloudflare.com/hc/en-us/articles/206973867-Error-analytics-by-colo
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,44 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Landmark.OLA.Models;
using Serilog;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoggingController : ControllerBase
{
public LoggingController()
{
}
/// <summary>
/// Use to send something to the logger from the client
/// </summary>
/// <remarks>
/// Logs a message from a client.
/// Cookies: None required or set.
/// Security: None set or required [yet]. It may be best to require a token here, but then we'll miss startup errors.
/// </remarks>
// Post: api/Logging/Error
[HttpPost("Error")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(403)]
[ProducesResponseType(500)]
public IActionResult Post([FromBody] ClientError error)
{
try
{
Log.Error(error.Message, error.StackTrace);
return Ok();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,8 @@
namespace Landmark.OLA.Application.Controllers
{
public class OnlineIdentityFormData
{
public string DateOfBirth { get; set; }
public string SocialSecurityNumber { get; set; }
}
}
@@ -0,0 +1,79 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Serilog;
using Landmark.OLA.Models;
using Landmark.OLA.Application.Services;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/GetPdf/")]
[ApiController]
public class PdfController : ControllerBase
{
private readonly IPdfService _pdfService;
private readonly ApplicationControlConfiguration _appControlConfig;
public PdfController(IOptions<ApplicationControlConfiguration> settings,
IPdfService pdfService)
{
_pdfService = pdfService;
_appControlConfig = settings.Value;
}
// Get: api/GetPdf/ImportantTerms
[HttpGet("ImportantTerms")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public IActionResult GetPdfImportantTerms()
{
try
{
var result = _pdfService.GetFileBytes(_appControlConfig.ImportantTermsPdfFilePath);
var payload = new FileContentResult(result, "application/octet-stream");
return payload;
}
catch (ArgumentException ae)
{
Log.Error(ae, $"Request for bad file path for IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf");
return BadRequest("File path not found");
}
catch (Exception ex)
{
Log.Error(ex, $"Unexpected error retrieving file path IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
// Get: api/GetPdf/WhatYouShouldKnow
[HttpGet("WhatYouShouldKnow")]
[ProducesResponseType(200)]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public IActionResult GetPdfWhatYouShouldKnow()
{
try
{
var result = _pdfService.GetFileBytes(_appControlConfig.WhatYouShouldKnowPdfFilePath);
var payload = new FileContentResult(result, "application/octet-stream");
return payload;
}
catch (ArgumentException ae)
{
Log.Error(ae, $"Request for bad file path What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf");
return BadRequest("File path not found");
}
catch (Exception ex)
{
Log.Error(ex, $"Unexpected error retrieving file path What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,213 @@
using System;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Security.Authentication;
using Serilog;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc;
using OnlineBankingAuth.Tokens;
using Landmark.OLA.Models.OnlineBanking;
using Landmark.OLA.Application.Common.Security;
using Landmark.OLA.Models.Core;
using OnlineBankingAuth.Parsers;
using OnlineBankingAuth.Services;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class SamlAssertionConsumerController : ControllerBase
{
private readonly IDigitalInsightVendorClient _diVendor;
private readonly ITokenService _tokenService;
public SamlAssertionConsumerController
(
IDigitalInsightVendorClient diVendor,
ITokenService tokenService
)
{
_diVendor = diVendor;
_tokenService = tokenService;
}
/// <summary>
/// Use to refer a user to the application with a valid SAML assertion
/// </summary>
/// <remarks>
/// Returns an application redirect, adding in a SAML assertion via a JWT in a secure http-only cookie.
/// Cookies: Set a standard pair of authentication and authorization cookies.
/// Security: Passed document must include a valid, signed SAML assertion, provides member authentication.
/// </remarks>
// POST: api/SAMLAssertionConsumer
[HttpPost]
[ProducesResponseType(403)]
[ProducesResponseType(401)]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(ModelStateDictionary), 401)]
public async Task Post([FromForm] SAMLMessaging document)
{
try
{
var decodedString = Encoding.UTF8.GetString(Convert.FromBase64String(document.SAMLResponse));
var parsedXml = XElement.Parse(decodedString);
var xmlDocument = new XmlDocument
{
PreserveWhitespace = true
};
xmlDocument.Load(parsedXml.CreateReader());
// @TODO: Should catch error here, move element query to static service #1400b
var x509Payload = (XmlElement)xmlDocument.GetElementsByTagName("ds:X509Certificate")[0];
var vendorCertMatches = _diVendor.ValidateDIPublicKey(x509Payload);
if (!vendorCertMatches)
{
throw new AuthenticationException("The vendor's public certificate did not match! ");
}
var assertion = _diVendor.GetSAMLPayload(xmlDocument);
// Check that assertion is still valid
var datesValid = _diVendor.ValidateTimeStamp(assertion);
if (!datesValid)
{
throw new AuthenticationException("The Authentication has expired!");
}
var obToken = new OnlineBankingToken(assertion, null, true)
{
IsTwoFactor = true
};
obToken.IndividualID = obToken.MemberID;
AssignTokens(obToken);
HttpContext.Response.Redirect(_diVendor.GetSuccessRedirectUrl());
}
catch (Exception ex)
{
Log.Error(ex, $"Failure to validate SAML in {GetType().Name}");
HttpContext.Response.Redirect(_diVendor.GetFailureRedirectUrl(ex.Message));
}
}
private void AssignTokens(OnlineBankingToken obToken)
{
var meta = new MemberMeta
{
IsTwoFactor = obToken.IsTwoFactor,
IsMember = (obToken.MemberID != null),
IsIndividual = (obToken.IndividualID != null)
};
var privateToken = _tokenService.CreateOnlineBankingJWT(obToken);
var publicToken = _tokenService.CreateAuthMetaToken(meta);
CookieService.SetAuthenticationCookie(HttpContext.Response.Cookies, privateToken);
CookieService.SetAuthorizationCookie(HttpContext.Response.Cookies, publicToken);
}
/// <summary>
/// Use to create an empty cookie session
/// </summary>
/// <remarks>
/// Returns a pair of empty authorization and authentication cookies.
/// Cookies: Sets empty Authentication and Authorization cookies.
/// Security: None required or provided.
/// </remarks>
[HttpGet("InitSession")]
[ProducesResponseType(200)]
public IActionResult Get()
{
var obToken = new OnlineBankingToken
{
MemberID = null, IndividualID = null, FI_ID = "0", IsTwoFactor = false
};
AssignTokens(obToken);
return Ok();
}
/// <summary>
/// Use to create a cookie session with credentials
/// </summary>
/// <remarks>
/// Returns a pair of authorization and authentication cookies when provided with valid credentials.
/// Cookies: Sets Authentication and Authorization cookies.
/// Security: Require.
/// </remarks>
[HttpPost("Authenticate")]
[ProducesResponseType(403)]
[ProducesResponseType(201)]
[ProducesResponseType(typeof(ModelStateDictionary), 401)]
public async Task<object> PostAsync([FromBody] OnlineBankingFormData document)
{
try
{
var diResponse = await _diVendor.AuthenticateOnlineBankingAccount(document);
if (diResponse.Token == null)
{
Log.Error($"Member failed non-two factor login, {@document.Username}");
return Ok(diResponse.Status);
}
var assertion = new DISAMLAssertion
{
MemberID = diResponse.Token.DiMemberNumber,
FI_ID = diResponse.Token.DiFiid
};
var obToken = new OnlineBankingToken(assertion, null, false)
{
IsTwoFactor = false
};
obToken.IndividualID = obToken.MemberID;
AssignTokens(obToken);
return Ok(diResponse.Status);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure to authenticate user in {GetType().Name}");
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to identify a unique individual on the member account.
/// </summary>
/// <remarks>
/// Returns OK if an individual ID was added to the cookie.
/// Cookies: Sets Individual ID in cookie.
/// Security: Requires existing cookie, with member.
/// </remarks>
[HttpPost("Identify")]
[ProducesResponseType(403)]
[ProducesResponseType(400)]
[ProducesResponseType(200)]
[ProducesResponseType(typeof(ModelStateDictionary), 401)]
public async Task<object> PostIdentify([FromBody] OnlineIdentityFormData document)
{
try
{
var token = _tokenService.GetAuthenticatedDataFromCookie(HttpContext);
if (token == null)
{
Log.Error($"Non-authenticated attempt at individual confirmation! Using info {@document}, Refusing.");
return Unauthorized();
}
token.IndividualID = token.MemberID;
AssignTokens(token);
return Ok();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure to identify user in {GetType().Name}");
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,92 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Landmark.OLA.Application.Services;
using Landmark.OLA.Models;
using Serilog;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly IStatus _status;
private readonly ApplicationControlConfiguration _appControlConfig;
private readonly CoreSettings _coreSettings;
public StatusController(IOptions<ApplicationControlConfiguration> settings,
IStatus status,
CoreSettings coreSettings)
{
_appControlConfig = settings.Value;
_status = status;
_coreSettings = coreSettings;
}
/// <summary>
/// Attempts to load the root of the API result in a web-app redirect.
/// </summary>
/// <remarks>
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// GET: Root
[HttpGet()]
[ProducesResponseType(302)]
public void GetRoot()
{
HttpContext.Response.Redirect(_appControlConfig.HomePageUrl);
}
/// <summary>
/// Use to get the status of the api
/// </summary>
/// <remarks>
/// Returns a simple object describing the build time, version, and environment of the running API.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// GET: api/Status
[HttpGet("api/Status")]
[ProducesResponseType(typeof(IStatus), 200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
return new ObjectResult(_status);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to get the core connection settings of the api
/// </summary>
/// <remarks>
/// Returns a simple object describing the core settings being used by the API.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// GET: api/Status
[HttpGet("api/GetCoreConnection")]
[ProducesResponseType(typeof(CoreSettings), 200)]
[ProducesResponseType(500)]
public IActionResult GetCoreConnection()
{
try
{
return new ObjectResult(_coreSettings);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int) System.Net.HttpStatusCode.InternalServerError);
}
}
}
}
@@ -0,0 +1,577 @@
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> blobStorageSettings,
IBlobStorageAccess blobStorageAccess)
{
loanApplicationStorage = loanApplicationDataAccess;
_tokenService = tokenService;
_blobStorageSettings = blobStorageSettings.Value;
_blobStorageAccess = blobStorageAccess;
}
/// <summary>
/// Use to retrieve a set of Loan Containers by filter
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
// Post: api/Storage/Filter
[HttpPost("Filter")]
[ProducesResponseType(typeof(IEnumerable<LoanContainer>), 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);
}
}
/// <summary>
/// Use to get a specific loan container FOR AN ADMIN
/// </summary>
/// <remarks>
/// Returns a loan container given a loan id.
/// Cookies: None required or set.
/// Security: Must have a valid JWT token retrieved from Azure AD.
/// </remarks>
// 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);
}
}
/// <summary>
/// Use to get loan containers for an authenticated member
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
// GET: api/Storage
[HttpGet("GetMemberApplications", Name = "GetMemberApplications")]
[ProducesResponseType(typeof(List<LoanContainer>), 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<LoanContainer> containers = loanApplicationStorage.GetAllMemberContainers(assertion.MemberID, assertion.IndividualID);
return Ok(containers);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to save a loan container
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
// 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);
}
}
/// <summary>
/// Use update a loan container
/// </summary>
/// <remarks>
/// Returns a status after attempting to update a loan container with new info.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// 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);
}
}
/// <summary>
/// Use to delete a loan
/// </summary>
/// <remarks>
/// Deletes a loan given an id.
/// Cookies: None required or set.
/// Security: Requires JWT token from Azure AD, AND Admin role.
/// </remarks>
// 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);
}
}
/// <summary>
/// Use to delete an entire loan container
/// </summary>
/// <remarks>
/// Deletes a loan container given a container id.
/// Cookies: None required or set.
/// Security: Requires JWT token from Azure AD, AND Admin role.
/// </remarks>
// 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<LoanApplicationFile> filesDeleted = new List<LoanApplicationFile>();
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);
}
}
}
}
@@ -0,0 +1,165 @@
using System;
using System.Net;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Landmark.OLA.Application.Common.Domain;
using Landmark.OLA.Application.DataAccess;
namespace Landmark.OLA.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UiSettingsController : ControllerBase
{
private readonly UiSettings _uiSettings;
private readonly IBranchDataAccess _branchStorage;
private readonly IConfigurationAccess _configurationAccess;
public UiSettingsController(UiSettings uiSettings,
IBranchDataAccess branchDataAccess,
IConfigurationAccess configurationAccess)
{
_uiSettings = uiSettings;
_branchStorage = branchDataAccess;
_configurationAccess = configurationAccess;
}
/// <summary>
/// Use to retrieve UI Settings
/// </summary>
/// <remarks>
/// Returns the UI Settings; with the dynamic branch mappings injected.
/// Cookies: None required or set.
/// Security: None required or provided.
/// </remarks>
// GET: api/Content/
[HttpGet]
[ProducesResponseType(typeof(UiSettings), 200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
var settings = _uiSettings;
settings.BranchMapping = _branchStorage.GetBranchConfig();
settings.BranchNameMapping = _branchStorage.GetBranchNameConfig();
settings.Configuration = _configurationAccess.GetStoredSettings();
return Ok(settings);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to get stored config (run-time changeable)
/// </summary>
/// <remarks>
/// Returns the current StoredConfig
/// Cookies: None required or set.
/// Security: Requires Azure AD token and Admin role.
/// </remarks>
// GET: api/UiSettings/StoredSettings
[HttpGet("StoredSettings", Name = "GetStoredSettings")]
[ProducesResponseType(typeof(StoredSettings), 200)]
[ProducesResponseType(500)]
//[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")]
public IActionResult GetStoredSettings()
{
try
{
var config = _configurationAccess.GetStoredSettings();
return Ok(config);
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use to save the initial storedConfig.
/// </summary>
/// <remarks>
/// Returns a saved configuration with id. This call will fail if ANY configs currently exist.
/// Cookies: None required or set.
/// Security: Requires Azure AD Jwt token and admin role.
/// </remarks>
// POST: api/UiSettings/StoredSettings
[HttpPost("StoredSettings", Name = "CreateStoredSettings")]
[ProducesResponseType(typeof(string), 201)]
[ProducesResponseType(403)]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)]
[ProducesResponseType(500)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")]
public object CreateStoredSettings([FromBody] StoredSettings config)
{
try
{
if (config != null)
{
config.Id = (Guid.NewGuid()).ToString();
config.LastUpdated = DateTime.Now;
_configurationAccess.CreateStoredSettings(config);
HttpContext.Response.StatusCode = 201;
return new { id = config.Id, message = "Successfully saved configuration" };
}
return BadRequest("StoredConfig is required.");
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Use update the StoredConfig
/// </summary>
/// <remarks>
/// Returns a status after attempting to update the StoredConfig.
/// Cookies: None required or set.
/// Security: Must have a valid JWT token retrieved from Azure AD, with ADMIN role.
/// </remarks>
// PUT: api/UiSettings/StoredSettings
[HttpPut("StoredSettings", Name = "UpdateStoredSettings")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary), 400)]
[ProducesResponseType(403)]
[ProducesResponseType(500)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Roles = "Admin")]
public IActionResult UpdateStoredSettings([FromBody] StoredSettings config)
{
try
{
var cosmosId = config?.Id;
if (!String.IsNullOrEmpty(cosmosId))
{
config.LastUpdated = DateTime.Now;
_configurationAccess.UpdateStoredSettings(config);
return CreatedAtAction(nameof(UpdateStoredSettings), new { id = config.Id }, config);
}
return BadRequest("StoredSettings invalid.");
}
catch (KeyNotFoundException ex)
{
Log.Error(ex.Message);
return NotFound();
}
catch (Exception e)
{
Log.Error(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
}
}