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,10 @@
namespace Landmark.OLA.Application.Common.Domain
{
public class EmailRequest
{
public string Recipient { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models;
namespace Landmark.OLA.Application.Common.Domain
{
public class StoredSettings : OlaModel
{
public DateTime LastUpdated { get; set; }
[Required]
public string LastUpdatedBy { get; set; }
public bool ApplicationDisabled { get; set; }
public string AppDisabledMessage { get; set; }
}
}
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Landmark.OLA.Models;
using Landmark.OLA.Models.Addresses.Enums;
using Landmark.OLA.Models.Individuals.Enums;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Enums;
using static Landmark.OLA.Models.Loan.TermLengths;
namespace Landmark.OLA.Application.Common.Domain
{
public class UiSettings
{
public string AbaRouting => "275079714";
public Dictionary<string, string> Contact => new Dictionary<string, string>
{
{ "HelpPhone", "262-796-4500" },
};
public Dictionary<string, string> Links => new Dictionary<string, string>
{
{ "PrivacyPolicy", "https://www.example.com/" },
{ "MemberSite", "https://www.example.net" },
{ "HomePage", "https://www.landmarkcu.com/"},
};
public Dictionary<decimal, List<int>> MfdHomeAndRvByAmount = TermLengths.MfdHomeAndRvByAmount;
public List<int> HomeEquity = TermLengths.HomeEquity;
public List<int> Special = TermLengths.Special;
public List<int> MortgageRefiExpress = TermLengths.MortgageRefiExpress;
public List<int> VehiclePreApproval = TermLengths.VehiclePreApproval;
public Dictionary<int, List<int>> TermsByYear = TermLengths.TermsByYear;
public Dictionary<decimal, List<int>> BoatLoansByAmount = TermLengths.BoatLoansByAmount;
public Dictionary<decimal, List<int>> PowersportByAmount = TermLengths.PowersportByAmount;
public Dictionary<decimal, List<int>> SignatureByAmount = TermLengths.SignatureByAmount;
public Dictionary<LoanType, List<LoanPurpose.Purpose>> TypePurposes = LoanPurpose.TypePurposes;
public Dictionary<LoanType, Limit> Limits = LoanLimits.Limits;
public List<StateOption> StatesRequiringSpouseInformation = ValidationConstants.StatesRequiringSpouseInformation;
public List<StateOption> StateOptions = ((StateOption[])Enum.GetValues(typeof(StateOption))).ToList();
public Dictionary<Frequency, Limit> IncomeLimits = Models.Individuals.IncomeLimits.Limits;
public BranchConfig BranchMapping = null; // Provided by MongoDB
public BranchNameConfig BranchNameMapping = null; // Provided by MongoDB
public StoredSettings Configuration = null; // Provided by MongoDB
public List<UiTermOption> TermOptions = AvailableTermOptions;
public List<UiTermOption> TermOptionsNoYear = AvailableTermOptionsWithoutYears;
}
}
@@ -0,0 +1,12 @@
namespace Landmark.OLA.Application.Common.Security
{
public class AzureActiveDirectoryOptions
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Instance { get; set; }
public string Domain { get; set; }
public string TenantId { get; set; }
public string ValidAudience { get; set; }
}
}
@@ -0,0 +1,49 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Landmark.OLA.Application.Common.Security
{
public static class AzureAdServiceCollectionExtensions
{
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
=> builder.AddAzureAdBearer(_ => { });
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureActiveDirectoryOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
builder.AddJwtBearer();
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureActiveDirectoryOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureActiveDirectoryOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = _azureOptions.ValidAudience
};
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
}
}
@@ -0,0 +1,70 @@
using System;
using Microsoft.AspNetCore.Http;
using Landmark.OLA.Models.OnlineBanking;
namespace Landmark.OLA.Application.Common.Security
{
public class CookieService
{
public static void SetAuthenticationCookie(IResponseCookies cookies, string token)
{
if (!string.IsNullOrEmpty(token))
{
var expiry = new DateTimeOffset(DateTime.UtcNow.AddMinutes(15));
var options = new CookieOptions
{
SameSite = SameSiteMode.Lax,
HttpOnly = true,
Secure = true,
Expires = expiry,
Domain = "landmarkcu.com"
};
cookies
.Append(Constants.TokenPayload, token, options);
}
}
public static void SetAuthorizationCookie(IResponseCookies cookies, string authorization)
{
if (!string.IsNullOrEmpty(authorization))
{
var expiry = new DateTimeOffset(DateTime.UtcNow.AddMinutes(15));
var options = new CookieOptions
{
SameSite = SameSiteMode.Lax,
HttpOnly = false,
Secure = true,
Expires = expiry,
Domain = "landmarkcu.com",
};
cookies
.Append(Constants.TokenAuthorization, authorization, options);
}
}
public static void ExpireCookies(HttpContext context)
{
var currentCookies = context.Request.Cookies;
var expiry = new DateTimeOffset(DateTime.UtcNow.AddDays(-1));
var options = new CookieOptions
{
HttpOnly = false,
Secure = true,
Expires = expiry,
Domain = ".landmarkcu.com"
};
if (currentCookies[Constants.TokenPayload] != null)
{
context.Response.Cookies
.Delete(Constants.TokenPayload, options);
}
if (currentCookies[Constants.TokenAuthorization] != null)
{
context.Response.Cookies
.Delete(Constants.TokenAuthorization, options);
}
}
}
}
@@ -0,0 +1,22 @@
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.Common.Security
{
public static class CosmosIdService
{
public static LoanContainer AppendCosmosId(LoanContainer loanContainer, string cosmosId)
{
if (!string.IsNullOrEmpty(cosmosId))
{
loanContainer.Id = cosmosId;
}
return loanContainer;
}
public static LoanContainer RemoveCosmosId(LoanContainer loanContainer)
{
loanContainer.LoanId = null;
return loanContainer;
}
}
}
@@ -0,0 +1,26 @@
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Http;
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.Common.Security
{
public static class ForwardedHeaderService
{
public static string GetRemoteIPAddress(HttpContext context)
{
var header = (context.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? context.Request.Headers["X-Forwarded-For"].FirstOrDefault());
if (IPAddress.TryParse(header, out IPAddress ip))
{
return ip.ToString();
}
return context.Connection.RemoteIpAddress.ToString();
}
public static LoanContainer AppendIPAddress(LoanContainer loanContainer, HttpContext context)
{
loanContainer.LoanApplication.IPAddress = GetRemoteIPAddress(context);
return loanContainer;
}
}
}
@@ -0,0 +1,28 @@
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.OnlineBanking;
namespace Landmark.OLA.Application.Common.Security
{
public static class MemberIdService
{
public static LoanContainer AppendMemberInfo(LoanContainer loanContainer, OnlineBankingToken token)
{
if (!string.IsNullOrEmpty(token?.MemberID) )
{
loanContainer.MemberId = token.MemberID;
}
if (!string.IsNullOrEmpty(token?.IndividualID) )
{
loanContainer.IndividualId = token.IndividualID;
}
return loanContainer;
}
public static LoanContainer RemoveMemberId(LoanContainer loanContainer)
{
loanContainer.MemberId = null;
loanContainer.IndividualId = null;
return loanContainer;
}
}
}
@@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Serilog;
using OnlineBankingAuth.Tokens;
namespace Landmark.OLA.Application.Common.Security
{
public class RenewAuthenticatedSessionMiddleware
{
private readonly RequestDelegate _next;
public RenewAuthenticatedSessionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ITokenService tokenService)
{
await _next(context);
try
{
CookieService.SetAuthenticationCookie(context.Response.Cookies, tokenService.RenewOnlineBankingJWT(context));
CookieService.SetAuthorizationCookie(context.Response.Cookies, tokenService.RenewAuthorization(context));
}
catch(Exception ex)
{
Log.Fatal("Error renewing cookied", ex);
// Could not refresh token :-( no biggie?
}
}
}
}
@@ -0,0 +1,13 @@
namespace Landmark.OLA.Application.Configuration
{
public class ApplicationSettings
{
public string AppName { get; set; }
public string Environment { get; set; }
public string SeqUrl { get; set; }
public string SeqKey { get; set; }
public string LogFilePath { get; set; }
public string LogFileInterval { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Landmark.OLA.Application.Configuration
{
public class BlobStorageConnectionSettings
{
public string AccountName { get; set; }
public string AccountKey { get; set; }
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Landmark.OLA.Application.Configuration
{
public class BlobStorageSettings
{
public float MaxFileSizeInMB { get; set; }
public List<string> AllowedFileTypes { get; set; }
public int FileLimit { get; set; }
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Landmark.OLA.Application.Configuration
{
public class DealershipEmailSettings
{
public string RedBannerMessage { get; set; }
public List<string> Recipients { get; set; }
public string ActionLink { get; set; }
public string EmailAPIUrl { get; set; }
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Landmark.OLA.Application.Configuration
{
public class NotificationsApiConfiguration
{
public string Url { get; set; }
public string SendEmail { get; set; }
public List<string> ErrorRecipients { get; set; } = [];
[NotMapped]
public string SendEmailEndpointUrl => Url + SendEmail;
}
}
@@ -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);
}
}
}
}
@@ -0,0 +1,72 @@
using System;
using Microsoft.Extensions.Options;
using Azure.Storage.Blobs;
using Landmark.OLA.Application.Configuration;
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.DataAccess
{
public class BlobStorageAccess : IBlobStorageAccess
{
readonly BlobServiceClient _blobServiceClient;
readonly string _containerName;
readonly BlobContainerClient _containerClient;
readonly BlobStorageConnectionSettings _settings;
public BlobStorageAccess(IOptions<BlobStorageConnectionSettings> settings)
{
_settings = settings.Value;
_blobServiceClient = new BlobServiceClient($"AccountName={_settings.AccountName};AccountKey={_settings.AccountKey}");
_containerName = "documents";
_containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
}
public byte[] GetBlobAsBytes(LoanApplicationFile file)
{
if (file == null)
{
return [];
}
var blob = _containerClient.GetBlobClient(file.GetBlobPath());
var fileContent = blob.DownloadContent();
var fileBytes = fileContent.Value.Content.ToArray();
return fileBytes;
}
public string GetBlobAsBase64(LoanApplicationFile file)
{
var fileBytes = GetBlobAsBytes(file);
return Convert.ToBase64String(fileBytes);
}
public bool SaveBlob(LoanApplicationFile file, byte[] fileBytes)
{
if(file == null || fileBytes == null || fileBytes.Length == 0)
{
return false;
}
BlobClient blobClient = _containerClient.GetBlobClient(file.GetBlobPath());
blobClient.Upload(new BinaryData(fileBytes));
blobClient.SetMetadata(file.GetMetadata());
return true;
}
public bool DeleteBlob(LoanApplicationFile file)
{
if (file == null)
{
return false;
}
BlobClient blobClient = _containerClient.GetBlobClient(file.GetBlobPath());
return blobClient.DeleteIfExists();
}
}
}
@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using CRIF.ACTion.Configuration;
using CRIF.ACTion.Services.Crif;
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.DataAccess
{
public class BranchDataAccess : IDisposable, IBranchDataAccess
{
private bool disposed = false;
private protected readonly CrifSettings _crifSettings;
private readonly IDataClient _client;
private readonly ICrifVendorClient _vendorClient;
private readonly string _collectionName = "Branches";
private readonly string _branchNameCollectionName = "ServicingChannelToBranchName";
private readonly List<BranchNameData> _branchNameDataListDefaults = new List<BranchNameData>
{
new() { ChannelId = 0, BranchName = "Not Found" },
new() { ChannelId = 1, BranchName = "ALL" }
};
public BranchDataAccess(
IDataClient dataClient,
IOptions<CrifSettings> crifSettings,
ICrifVendorClient vendorClient)
{
_client = dataClient;
_crifSettings = crifSettings.Value;
_vendorClient = vendorClient;
}
private IMongoCollection<BranchConfig> GetBranchContainersCollection()
{
return _client.GetDatabase().GetCollection<BranchConfig>(_collectionName);
}
private IMongoCollection<BranchNameConfig> GetBranchNameContainersCollection()
{
return _client.GetDatabase().GetCollection<BranchNameConfig>(_branchNameCollectionName);
}
public BranchConfig GetBranchConfig()
{
try
{
var collection = GetBranchContainersCollection();
var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault();
return result ?? throw new Exception("Could not find branch mapping");
}
catch (MongoConnectionException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Could not load branch config");
}
}
public void CreateBranchConfig(BranchConfig branchConfig)
{
try
{
var collection = GetBranchContainersCollection();
if (collection.Find(new BsonDocument()).ToList().Count > 0)
{
throw new Exception("Can only insert 1 branch config");
}
collection.InsertOne(branchConfig);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while creating branch config");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while creating branch config");
}
}
public void UpdateBranchConfig(BranchConfig branchConfig)
{
try
{
var collection = GetBranchContainersCollection();
ReplaceOneResult result = collection.ReplaceOne(eachLoan => eachLoan.Id == branchConfig.Id, branchConfig);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while creating updating branch config");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while creating updating branch config");
}
}
public BranchNameConfig GetBranchNameConfig()
{
try
{
var collection = GetBranchNameContainersCollection();
var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault();
if (result == null)
{
UpsertBranchNameConfig();
}
else if (result != null && result.BranchNamesByChannelId.Count <= _branchNameDataListDefaults.Count)
{
UpsertBranchNameConfig(true);
}
collection = GetBranchNameContainersCollection();
result = collection.Find(new BsonDocument()).ToList().FirstOrDefault();
return result;
}
catch (MongoConnectionException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Could not load branch name config");
}
}
public void UpsertBranchNameConfig(bool IsUpdate = false)
{
try
{
var collection = GetBranchNameContainersCollection();
if (collection.Find(new BsonDocument()).ToList().Count > 0 && !IsUpdate)
{
throw new Exception("Can only insert 1 branch name config");
}
SortedList<int, string> configurationDetails = _vendorClient.GetBranchNameConfiguration("getChannels").GetAwaiter().GetResult();
BranchNameConfig branchNameConfig = new();
List<BranchNameData> branchNameDataList = CreateBranchNameDataList(configurationDetails);
branchNameConfig.Id = (Guid.NewGuid()).ToString();
branchNameConfig.Date = DateTime.Now;
branchNameConfig.BranchNamesByChannelId = branchNameDataList;
if (IsUpdate)
{
var oldConfig = collection.Find(new BsonDocument()).ToList().FirstOrDefault();
branchNameConfig.Id = oldConfig.Id;
ReplaceOneResult result = collection.ReplaceOne(eachConfig => eachConfig.Id == branchNameConfig.Id, branchNameConfig);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
else
{
collection.InsertOne(branchNameConfig);
}
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while creating branch names config");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while creating branch names config");
}
}
public void UpdateBranchNameConfig()
{
UpsertBranchNameConfig(true);
}
public List<BranchNameData> CreateBranchNameDataList(SortedList<int, string> configurationDetails)
{
List<BranchNameData> branchNameDataList = _branchNameDataListDefaults;
foreach (var branchNameData in configurationDetails)
{
try
{
branchNameDataList.Add(new BranchNameData { ChannelId = branchNameData.Key, BranchName = branchNameData.Value });
}
catch (Exception ex)
{
Log.Error(ex.Message);
}
}
return branchNameDataList;
}
#region IDisposable
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
}
}
this.disposed = true;
}
public int GetDistributionChannelByZipCode(string zipCode)
{
var config = GetBranchConfig();
foreach (var eachBranch in config.BranchCodesByZip)
{
if (eachBranch.Zipcode == zipCode)
{
return eachBranch.ChannelId;
}
}
return _crifSettings.ServicingChannels.DefaultServicingChannel;
}
#endregion
}
}
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using Landmark.OLA.Application.Common.Domain;
namespace Landmark.OLA.Application.DataAccess
{
public class ConfigurationAccess : IDisposable, IConfigurationAccess
{
private bool disposed = false;
private readonly IDataClient _client;
private readonly string _collectionName = "AppConfiguration";
public ConfigurationAccess(
IDataClient dataClient)
{
_client = dataClient;
}
private IMongoCollection<StoredSettings> GetConfigurationContainersCollection()
{
return _client.GetDatabase().GetCollection<StoredSettings>(_collectionName);
}
public StoredSettings GetStoredSettings()
{
try
{
var collection = GetConfigurationContainersCollection();
var result = collection.Find(new BsonDocument()).ToList().FirstOrDefault();
if( result == null )
{
throw new Exception("Could not find app configuration.");
}
return result;
}
catch (MongoConnectionException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Could not load app configuration.");
}
}
public void CreateStoredSettings(StoredSettings storedSettings)
{
try
{
var collection = GetConfigurationContainersCollection();
if (collection.Find(new BsonDocument()).ToList().Count > 0)
{
throw new Exception("Can only insert 1 configuration");
}
collection.InsertOne(storedSettings);
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while creating app configuration");
}
}
public void UpdateStoredSettings(StoredSettings storedSettings)
{
try
{
var collection = GetConfigurationContainersCollection();
ReplaceOneResult result = collection.ReplaceOne(eachConfig => eachConfig.Id == storedSettings.Id, storedSettings);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while updating app configuration");
}
}
#region IDisposable
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
}
}
this.disposed = true;
}
#endregion
}
}
@@ -0,0 +1,13 @@
namespace Landmark.OLA.Application.DataAccess
{
public class DataAccessConnection
{
public string DatabaseName { get; set; }
public string UserName { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string Password { get; set; }
}
}
@@ -0,0 +1,45 @@
using System.Security.Authentication;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
namespace Landmark.OLA.Application.DataAccess
{
// Reference: http://mongodb.github.io/mongo-csharp-driver/2.5
public class DataClient : IDataClient
{
private readonly MongoClientSettings _mongoSettings;
private readonly DataAccessConnection _configuration;
private readonly MongoClient _client;
private readonly IMongoDatabase _database;
public DataClient(IOptions<DataAccessConnection> settings) {
_configuration = settings.Value;
_mongoSettings = GetMongoSettings();
_client = new MongoClient(_mongoSettings);
_database = _client.GetDatabase(_configuration.DatabaseName);
}
public IMongoDatabase GetDatabase()
{
return _database;
}
private MongoClientSettings GetMongoSettings()
{
MongoClientSettings settings = new()
{
Server = new MongoServerAddress(_configuration.Host, _configuration.Port),
UseTls = true,
SslSettings = new SslSettings()
};
settings.SslSettings.EnabledSslProtocols = SslProtocols.Tls12;
MongoIdentity identity = new MongoInternalIdentity(_configuration.DatabaseName, _configuration.UserName);
MongoIdentityEvidence evidence = new PasswordEvidence(_configuration.Password);
settings.Credential = new MongoCredential("SCRAM-SHA-1", identity, evidence);
return settings;
}
}
}
@@ -0,0 +1,15 @@
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.DataAccess
{
public interface IBlobStorageAccess
{
bool SaveBlob(LoanApplicationFile file, byte[] fileBytes);
byte[] GetBlobAsBytes(LoanApplicationFile file);
string GetBlobAsBase64(LoanApplicationFile file);
bool DeleteBlob(LoanApplicationFile file);
}
}
@@ -0,0 +1,15 @@
using CRIF.ACTion.Services.Landmark.DistributionChannel;
using Landmark.OLA.Models.Loan;
namespace Landmark.OLA.Application.DataAccess
{
public interface IBranchDataAccess : IDistributionChannelLookupService
{
void Dispose();
BranchConfig GetBranchConfig();
void UpdateBranchConfig(BranchConfig branchConfig);
void CreateBranchConfig(BranchConfig branchConfig);
BranchNameConfig GetBranchNameConfig();
void UpdateBranchNameConfig();
}
}
@@ -0,0 +1,12 @@
using Landmark.OLA.Application.Common.Domain;
namespace Landmark.OLA.Application.DataAccess
{
public interface IConfigurationAccess
{
void Dispose();
StoredSettings GetStoredSettings();
void UpdateStoredSettings(StoredSettings branchConfig);
void CreateStoredSettings(StoredSettings branchConfig);
}
}
@@ -0,0 +1,9 @@
using MongoDB.Driver;
namespace Landmark.OLA.Application.DataAccess
{
public interface IDataClient
{
IMongoDatabase GetDatabase();
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Enums;
namespace Landmark.OLA.Application.DataAccessLayer
{
public interface ILoanApplicationDataAccess
{
void CreateContainer(LoanContainer loanContainer);
void DeleteApplication(string id);
void DeleteContainer(string id);
void Dispose();
List<LoanContainer> GetLoanContainers(Filter loanFilter);
List<LoanContainer> GetAllMemberContainers(string memberId, string individualId);
LoanContainer GetContainerById(string guid);
string GetContainerIdByLoanId(string loanId);
void UpdateContainer(LoanContainer loanContainer);
void UpdateStatus(string id, LoanStatus status);
void UpdateError(string id, LoanError error);
void UpdateLoanId(string id, string loanId);
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using LCU.Library.Internal.NotificationsModels.Models;
namespace Landmark.OLA.Application.DataAccess
{
public interface INotificationDao
{
Task SendEmailAsync(EmailRequest request);
}
}
@@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using Landmark.OLA.Application.Common.Security;
using Landmark.OLA.Application.Configuration;
using Landmark.OLA.Application.DataAccess;
using Landmark.OLA.Application.Services.MicroApiServices;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Enums;
using LCU.Library.Internal.NotificationsModels.Models;
namespace Landmark.OLA.Application.DataAccessLayer
{
public class LoanApplicationDataAccess : IDisposable, ILoanApplicationDataAccess
{
private bool disposed = false;
private readonly IDataClient _client;
private readonly INotificationsApiService _notificationsApiService;
private readonly string _collectionName = "Applications";
private readonly NotificationsApiConfiguration _notificationsApiConfiguration;
private readonly Services.Email.IEmailTemplateService _emailTemplateService;
public LoanApplicationDataAccess(
IDataClient dataClient,
INotificationsApiService notificationsApiService,
IOptions<NotificationsApiConfiguration> notificationsApiConfiguration,
IWebHostEnvironment webHostEnvironment,
Services.Email.IEmailTemplateService emailTemplateService)
{
_client = dataClient;
_notificationsApiService = notificationsApiService;
_notificationsApiConfiguration = notificationsApiConfiguration.Value;
_emailTemplateService = emailTemplateService;
}
public LoanContainer GetContainerById(string guid)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.Id == guid);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
return foundItems.First();
}
catch (MongoConnectionException)
{
return null;
}
}
public string GetContainerIdByLoanId(string loanId)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.LoanId == loanId);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
return foundItems.First().Id;
}
catch (MongoConnectionException)
{
return null;
}
}
public List<LoanContainer> GetAllMemberContainers(string memberId, string individualId)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.MemberId == memberId && eachContainer.IndividualId == individualId && eachContainer.LoanApplication != null).ToList();
foreach (var container in foundItems)
{
MemberIdService.RemoveMemberId(container);
}
return foundItems;
}
catch (MongoConnectionException)
{
return [];
}
}
public List<LoanContainer> GetAllContainers()
{
try
{
var collection = GetLoanContainersCollection();
return collection.Find(new BsonDocument()).ToList();
}
catch (MongoConnectionException)
{
return [];
}
}
public List<LoanContainer> GetLoanContainers(Filter filter)
{
try
{
var collection = GetLoanContainersCollection().Find(BuildFilter(filter)).Sort(BuildSorter(filter)).Limit(filter.Count).ToList();
return collection;
}
catch (MongoConnectionException)
{
return [];
}
}
public void CreateContainer(LoanContainer loanContainer)
{
try
{
var collection = GetLoanContainersCollection();
collection.InsertOne(loanContainer);
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while creating container");
}
}
public void UpdateContainer(LoanContainer loanContainer)
{
try
{
var collection = GetLoanContainersCollection();
ReplaceOneResult result = collection.ReplaceOne(eachLoan => eachLoan.Id == loanContainer.Id, loanContainer);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while updating container");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while updating container");
}
}
public void DeleteApplication(string id)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.Id == id);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
LoanContainer loanContainer = foundItems.First();
loanContainer.LoanApplication = null;
ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while deleting application");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while deleting application");
}
}
public void UpdateStatus(string id, LoanStatus status)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.Id == id);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
LoanContainer loanContainer = foundItems.First();
loanContainer.Status = status;
ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
if (status == LoanStatus.Vendor_Error || status == LoanStatus.Application_Error)
{
var reportId = id;
if ( loanContainer.LoanId != null)
{
reportId = loanContainer.LoanId;
}
var contentHtml = "There was a " + status + " in loan application: " + reportId + ". <br><br>";
var request = new EmailRequest()
{
Subject = "Vendor Error In Loan Application",
Recipient = _notificationsApiConfiguration.ErrorRecipients,
Body = _emailTemplateService.BuildBrandedBody(contentHtml)
};
_notificationsApiService.SendEmailAsync(request);
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while updating status");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while updating status");
}
}
public void UpdateError(string id, LoanError error)
{
try
{
var collection = GetLoanContainersCollection();
var foundItems = collection.Find(eachContainer => eachContainer.Id == id);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
LoanContainer loanContainer = foundItems.First();
loanContainer.Error = error;
ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while updating error");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while updating error");
}
}
public void UpdateLoanId(string id, string loanId)
{
var collection = GetLoanContainersCollection();
try
{
var foundItems = collection.Find(eachContainer => eachContainer.Id == id);
if (foundItems.CountDocuments() != 1)
{
throw new KeyNotFoundException("Item not found");
}
LoanContainer loanContainer = foundItems.First();
loanContainer.LoanId = loanId;
ReplaceOneResult result = collection.ReplaceOne(eachContainer => eachContainer.Id == loanContainer.Id, loanContainer);
if (result.MatchedCount == 0 || result.ModifiedCount == 0)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while updating loan id");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while updating loan id");
}
}
public void DeleteContainer(string id)
{
var collection = GetLoanContainersCollection();
try
{
DeleteResult result = collection.DeleteOne(_ => _.Id == id);
if (result.DeletedCount != 1)
{
throw new KeyNotFoundException("Item not found");
}
}
catch (KeyNotFoundException en)
{
Log.Error(en.Message);
throw new KeyNotFoundException(en.Message);
}
catch (MongoException ex)
{
Log.Error(ex.Message);
throw new InvalidDataException("Error occured while deleting container");
}
catch (Exception e)
{
Log.Error(e.Message);
throw new InvalidDataException("Error occured while deleting container");
}
}
private IMongoCollection<LoanContainer> GetLoanContainersCollection()
{
return _client.GetDatabase().GetCollection<LoanContainer>(_collectionName);
}
private static FilterDefinition<LoanContainer> BuildFilter(Filter filter)
{
DateTime sDate = filter.StartDate.ToUniversalTime();
DateTime eDate = filter.EndDate.ToUniversalTime();
var builder = Builders<LoanContainer>.Filter;
var filterValue = builder.Gt("Date", sDate) & builder.Lt("Date", eDate);
for (int i = 0; i < filter.FilterField.Length; i++)
{
var field = filter.FilterField[i];
if (field.NotEqual)
{
filterValue = filterValue & builder.Ne(field.Field, field.FilterValue);
}
else if (field.Equal)
{
filterValue = filterValue & builder.Eq(field.Field, field.FilterValue);
}
else if (field.GreaterThan)
{
filterValue = filterValue & builder.Gt(field.Field, field.FilterValue);
}
else if (field.LessThan)
{
filterValue = filterValue & builder.Lt(field.Field, field.FilterValue);
}
}
return filterValue;
}
private static string BuildSorter(Filter filter)
{
return "{ " + filter.SortField + ": " + (filter.SortDesc ? "-1}" : "1}");
}
#region IDisposable
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
}
}
this.disposed = true;
}
#endregion
}
}
@@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
using Landmark.OLA.Application.Configuration;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.NotificationsModels.Models;
using Microsoft.Extensions.Options;
namespace Landmark.OLA.Application.DataAccess
{
public class NotificationDao : INotificationDao
{
private readonly NotificationsApiConfiguration _configuration;
private readonly IPostRequest _postRequest;
public NotificationDao(IOptions<NotificationsApiConfiguration> configuration, IPostRequest postRequest)
{
_configuration = configuration.Value;
_postRequest = postRequest;
}
public async Task SendEmailAsync(EmailRequest request)
=> await _postRequest.Async(new Uri(_configuration.SendEmailEndpointUrl), request);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<TargetFramework>net8.0</TargetFramework>
<ApplicationIcon />
<OutputType>Exe</OutputType>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Folder Include="Files\" />
<Folder Include="wwwroot\" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="LCU.Library.Internal.LcuHttpClient" Version="2.0.1" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="2.2.0" />
<PackageReference Include="LCU.Library.Internal.NotificationsModels" Version="1.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.21" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.21" />
<PackageReference Include="Microsoft.Build" Version="17.8.43" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.21" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.24" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="0.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Http" Version="5.2.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CRIF.ACTion\CRIF.ACTion.csproj" />
<ProjectReference Include="..\Landmark.OLA.Models\Landmark.OLA.Models.csproj" />
<ProjectReference Include="..\OnlineBankingAuth\OnlineBankingAuth.csproj" />
<ProjectReference Include="..\USPS\USPS.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Files\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Files\IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Files\What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Landmark.OLA.Application.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,286 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Landmark.OLA.Application</name>
</assembly>
<members>
<member name="M:Landmark.OLA.Application.Controllers.AddressValidationController.Post(Landmark.OLA.Models.Addresses.Address)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.BranchConfigController.Get">
<summary>
Use to get the branch configuration
</summary>
<remarks>
Returns a full branch configuration.
Cookies: None required or set.
Security: None.
</remarks>
</member>
<member name="M:Landmark.OLA.Application.Controllers.BranchConfigController.Post(Landmark.OLA.Models.Loan.BranchConfig)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.BranchConfigController.Put(Landmark.OLA.Models.Loan.BranchConfig)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.DecisioningController.Put(System.String,Landmark.OLA.Models.Decisioning.DecisioningDetails)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.DecisioningController.Get(System.String)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.LoanApplicationController.Post(System.String)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.LoanApplicationController.Post(Landmark.OLA.Models.Loan.LoanApplication)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.LoggingController.Post(Landmark.OLA.Models.ClientError)">
<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: Must have a valid JWT token retrieved from Azure AD, with ADMIN role.
</remarks>
</member>
<member name="M:Landmark.OLA.Application.Controllers.PdfController.Get(System.String)">
<summary>
Use to get the pdfs
</summary>
<remarks>
Returns bytes of PDF.
Cookies: None required or set.
Security: None required or provided.
</remarks>
</member>
<member name="M:Landmark.OLA.Application.Controllers.SAMLAssertionConsumerController.Post(OnlineBankingAuth.Parsers.SAMLMessaging)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.SAMLAssertionConsumerController.Get">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.SAMLAssertionConsumerController.PostAsync(Landmark.OLA.Models.OnlineBanking.OnlineBankingFormData)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.SAMLAssertionConsumerController.PostIdentify(Landmark.OLA.Application.Controllers.OnlineIdentityFormData)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StatusController.GetRoot">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StatusController.GetCoreTest(System.String)">
<summary>
Attempts to load a website and return the reponse.
</summary>
<remarks>
Cookies: None required or set.
Security: None required or provided.
</remarks>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StatusController.Get">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.Post(Landmark.OLA.Models.Loan.Filter)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.Get(System.String)">
<summary>
Use to get a specific loan container
</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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.GetMemberApplications">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.Post(Landmark.OLA.Models.Loan.LoanContainer)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.Put(Landmark.OLA.Models.Loan.LoanContainer)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.DeleteLoan(System.String)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.StorageController.DeleteContainer(System.String)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.UiSettingsController.Get">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.UiSettingsController.GetStoredSettings">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.UiSettingsController.CreateStoredSettings(Landmark.OLA.Application.Common.Domain.StoredSettings)">
<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>
</member>
<member name="M:Landmark.OLA.Application.Controllers.UiSettingsController.UpdateStoredSettings(Landmark.OLA.Application.Common.Domain.StoredSettings)">
<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>
</member>
</members>
</doc>
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Landmark.OLA.Application
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.ConfigureAppConfiguration((context, builder) =>
{
var builtConfig = builder.Build();
builder.AddJsonFile("/app/config/environment-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/app/config/workload-appsettings/appsettings.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-primary-seq-key.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-usps-settings-username.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-application-storage-password.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-crif-settings-api-settings-username.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-private-cert-azure-thumbprint.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-public-cert-azure-thumbprint.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-token-secret.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-key.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-security-settings-digital-insight-consumer-secret.json", optional: true, reloadOnChange: true);
builder.AddJsonFile("/vault/secrets/online-loan-app-api-blob-storage-connection-settings-account-key.json", optional: true, reloadOnChange: true);
})
.UseSerilog();
}
}
@@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56932",
"sslPort": 44384
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Landmark.OLA.Application": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Landmark.OLA.Application.Controllers;
using Landmark.OLA.Models.Core;
namespace Landmark.OLA.Application.Services
{
public class CoreAccessService
{
public CoreMemberOwner FindMatchingOwner(OnlineIdentityFormData document, List<CoreMemberOwner> owners)
{
if (owners != null && owners.Count != 0 && document != null)
{
foreach (var owner in owners)
{
if (DoesOwnerMatch(owner, document))
{
return owner;
}
}
}
return null;
}
// Expects birthdate in form YYYY-MM-DD and taxid in form ###-##-####
// Expects dob in form MM/DD/YYYY and ssn in form #### (last 4)
// Tries to match this owner and this document.
public bool DoesOwnerMatch(CoreMemberOwner owner, OnlineIdentityFormData document)
{
if (string.IsNullOrEmpty(owner.BirthDate) || owner.TaxId == null || owner.TaxId.Length != 11)
{
return false;
}
if (owner.TaxId.Substring(7) != document.SocialSecurityNumber)
{
return false;
}
string[] formats = { "MM/dd/yyyy", "yyyy-MM-dd" };
var dobEntered = DateTime.ParseExact(document.DateOfBirth, formats, new CultureInfo("en-US"), DateTimeStyles.None);
var dobDesired = DateTime.ParseExact(owner.BirthDate, formats, new CultureInfo("en-US"), DateTimeStyles.None);
return dobEntered == dobDesired;
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using Serilog;
using Newtonsoft.Json;
using Landmark.OLA.Models.Core;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
namespace Landmark.OLA.Application.Services
{
public class CoreService : ICoreService
{
private readonly IGetRequest _getRequest;
public CoreService(IGetRequest getRequest)
{
_getRequest = getRequest;
}
public async Task<CoreIndividual> GetIndividual(string individualId)
{
try
{
var uri = new Uri($"core/individuals?userId={individualId}");
var responseString = await _getRequest.StringResponseAsync(uri);
return JsonConvert.DeserializeObject<CoreIndividual>(responseString);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure to get Individual for IndividualId: {individualId} in {GetType().Name}");
return null;
}
}
}
}
@@ -0,0 +1,9 @@
namespace Landmark.OLA.Application.Services
{
public class CoreSettings
{
public string EndPoint { get; set; }
public int Timeout { get; set; }
}
}
@@ -0,0 +1,113 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Serilog;
namespace Landmark.OLA.Application.Services.Email
{
public class EmailTemplateService : IEmailTemplateService
{
private readonly string _embeddedLogoBase64;
public EmailTemplateService(IWebHostEnvironment webHostEnvironment)
{
_embeddedLogoBase64 = LoadEmbeddedLogo(webHostEnvironment);
}
private static string LoadEmbeddedLogo(IWebHostEnvironment webHostEnvironment)
{
if (webHostEnvironment == null)
{
return string.Empty;
}
try
{
var logoPath = Path.Combine(webHostEnvironment.ContentRootPath, "Files", "Logo.png");
if (!File.Exists(logoPath))
{
Log.Warning("Logo not found at {LogoPath}. Email will fall back to remote image.", logoPath);
return string.Empty;
}
var bytes = File.ReadAllBytes(logoPath);
return Convert.ToBase64String(bytes);
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to embed logo from file.");
return string.Empty;
}
}
public string BuildBrandedBody(string contentHtml, string redBannerMessage = "")
{
var topLogoTag = $"<img src=\"data:image/png;base64,{_embeddedLogoBase64}\" width=\"225\" height=\"62\" style=\"width: 225px; height: 62px\">";
var bottomLogoTag = $"<img src=\"data:image/png;base64,{_embeddedLogoBase64}\" alt=\"Landmark Credit Union Logo\" height=\"37\" width=\"135\" style=\"width: 180px; max-height: 50px;\">";
redBannerMessage ??= string.Empty;
var bannerHtml = string.IsNullOrWhiteSpace(redBannerMessage)
? string.Empty
:
"<div class=\"red-banner\">" +
"<span style=\"background-color: #FF0000; color: #FFFFFF; font-weight: bold; display: inline-block; padding: 4px 8px;\">" +
redBannerMessage +
"</span><br><br>" +
"</div>";
var requestBody =
"<html>" +
"<body>" +
"<table align=\"center\" width=\"87%\" style=\"margin: auto;\">" +
"<tbody>" +
"<tr height=\"50\"></tr>" +
"<tr>" +
"<td align=\"center\" style=\"margin: auto; width: 250px\">" +
topLogoTag +
"</td>" +
"</tr>" +
"<tr height=\"30\"></tr>" +
"</tbody>" +
"</table>" +
"<table align=\"center\" width=\"87%\" style=\"margin: auto; color: #005674\">" +
"<tbody>" +
"<tr>" +
"<td style=\"font-size: 75%; font-family: Verdana; padding-top: 10px;\">" +
bannerHtml +
contentHtml +
"</td>" +
"</tr>" +
"<tr height=\"50\"></tr>" +
"</tbody>" +
"</table>" +
"<table style=\"text-align: center; color: #888; mso-color-alt: #888; font-size: 70%; background-color: #eee; width: 100%; position: absolute; bottom: 0; margin: auto;\" align=\"center\">" +
"<tbody>" +
"<tr>" +
"<td height=\"15\" colspan=\"4\">&nbsp;</td>" +
"</tr>" +
"<tr>" +
"<td height=\"100\" width=\"6.5%\">&nbsp;</td>" +
"<td height=\"100\" width=\"15%\">" +
"<div style=\"padding-right: 15px;\">" +
"<div style=\"margin: auto; display: flex; justify-content: center; align-items: center;\">" +
bottomLogoTag +
"</div>" +
"</div>" +
"</td>" +
"<td align=\"left\" style=\"padding-left: 25px; font-style: italic;\">" +
"</td>" +
"<td width=\"6.5%\">&nbsp;</td>" +
"</tr>" +
"<tr>" +
"<td height=\"15\" colspan=\"4\">&nbsp;</td>" +
"</tr>" +
"</tbody>" +
"</table>" +
"</body>" +
"</html>";
return requestBody;
}
}
}
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Hosting;
namespace Landmark.OLA.Application.Services.Email
{
public interface IEmailTemplateService
{
string BuildBrandedBody(string contentHtml, string redBannerMessage = "");
}
}
@@ -0,0 +1,12 @@
using System;
using System.Net.Http;
namespace Landmark.OLA.Application.Services.Exceptions
{
public class CoreUnavailableException : Exception
{
public CoreUnavailableException(HttpRequestException hre): base(hre.Message, hre)
{
}
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Landmark.OLA.Models.Core;
namespace Landmark.OLA.Application.Services
{
public interface ICoreService
{
Task<CoreIndividual> GetIndividual(string individualId);
}
}
@@ -0,0 +1,8 @@
namespace Landmark.OLA.Application.Services
{
public interface IPdfService
{
byte[] GetFileBytes(string path);
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using LCU.Library.Internal.NotificationsModels.Models;
namespace Landmark.OLA.Application.Services.MicroApiServices
{
public interface INotificationsApiService
{
Task SendEmailAsync(EmailRequest request);
}
}
@@ -0,0 +1,32 @@
using Serilog;
using System;
using System.Threading.Tasks;
using Landmark.OLA.Application.DataAccess;
using LCU.Library.Internal.NotificationsModels.Models;
namespace Landmark.OLA.Application.Services.MicroApiServices
{
public class NotificationsApiService : INotificationsApiService
{
private readonly INotificationDao _notificationsApiDao;
public NotificationsApiService(INotificationDao notificationsApiDao)
{
_notificationsApiDao = notificationsApiDao;
}
public async Task SendEmailAsync(EmailRequest request)
{
try
{
await _notificationsApiDao.SendEmailAsync(request);
}
catch (Exception exception)
{
var error = $"Failure during SendEmailAsync. Exception Message: {exception.Message}";
Log.Error(error);
throw new Exception(error);
}
}
}
}
@@ -0,0 +1,18 @@
using System;
using System.IO;
namespace Landmark.OLA.Application.Services
{
public class PdfService : IPdfService
{
public byte[] GetFileBytes(string path)
{
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
return File.ReadAllBytes(path);
}
throw new ArgumentException($"Path {path} must exist.");
}
}
}
@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Serilog;
using CRIF.ACTion.Common.Parsers;
using CRIF.ACTion.Configuration;
using CRIF.ACTion.Decisioning;
using CRIF.ACTion.Loan;
using CRIF.ACTion.Loan.Individuals;
using CRIF.ACTion.Services.Crif;
using Landmark.OLA.Application.Common.Domain;
using Landmark.OLA.Application.Common.Security;
using Landmark.OLA.Application.Configuration;
using Landmark.OLA.Application.DataAccess;
using Landmark.OLA.Application.DataAccessLayer;
using Landmark.OLA.Application.Services;
using Landmark.OLA.Models;
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Decisioning;
using Landmark.OLA.Models.Decisioning.Enums;
using Landmark.OLA.Models.Individuals;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.OnlineBanking;
using Landmark.OLA.Models.Vendor;
using OnlineBankingAuth.Communication;
using OnlineBankingAuth.Parsers;
using OnlineBankingAuth.Services;
using OnlineBankingAuth.Tokens;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.Logging.Models;
using LCU.Library.Internal.LcuHttpClient;
using Landmark.OLA.Application.Services.MicroApiServices;
using Landmark.OLA.Application.Services.Email;
namespace Landmark.OLA.Application
{
public class Startup(IConfiguration configuration)
{
public IConfiguration Configuration { get; } = configuration;
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var appConfig = new ApplicationSettings();
Configuration.GetSection("Primary").Bind(appConfig);
services.AddSingleton(appConfig);
var loggerConfiguration = new LoggingConfiguration(appConfig.AppName,
appConfig.SeqKey,
appConfig.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
Log.Information($"{appConfig.AppName} Starting Up");
services.AddLCUHttpServices();
services.AddCors(options =>
{
options.AddPolicy("DevCors", b => b
.SetIsOriginAllowed(_ => true)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
options.AddPolicy("ProdCors", b => b
.WithOrigins(
"https://applydev.landmarkcu.com",
"https://applyqa.landmarkcu.com",
"https://apply.landmarkcu.com",
"https://qa-online-loan-app.apps.azure.lcu-internal.com",
"https://uat-online-loan-app.apps.azure.lcu-internal.com",
"https://prd-online-loan-app.apps.azure.lcu-internal.com",
"https://qa-apps.landmarkcu.com",
"https://uat-apps.landmarkcu.com",
"https://apps.landmarkcu.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});
services.AddControllers().AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(new StringEnumConverter()));
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
//TODO #245: Disable in production?
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Landmark Loan API", Version = "v1" });
// For this to work, I also had to check the generate xml box in build options.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
});
services.AddScoped<ICrifVendorClient, CrifVendorClient>();
services.AddScoped<IStatus, Status>();
services.Configure<DataAccessConnection>(options =>
Configuration.GetSection("ApplicationStorage").Bind(options));
services.AddSingleton<IDataClient, DataClient>();
services.AddScoped<ILoanApplicationDataAccess, LoanApplicationDataAccess>();
services.AddScoped<INotificationDao, NotificationDao>();
services.AddScoped<IBranchDataAccess, BranchDataAccess>();
services.AddScoped<IConfigurationAccess, ConfigurationAccess>();
services.AddScoped<ICrifService, CrifService>();
services.AddScoped<IWcfInspectorBehavior, WcfInspectorBehavior>();
services.AddScoped<IResponseParser<Individual>, AddIndividualResponseParser>();
services.AddScoped<IResponseParser<LoanApplication>, LoanApplicationResponseParser>();
services.AddScoped<IResponseParser<DecisioningState>, DecisioningStateResponseParser>();
services.AddScoped<IResponseParser<DecisioningDetails>, GetApplicationProductsResponseParser>();
services.AddScoped<IResponseParser<SortedList<int, string>>, ConfigurationRequestResponseParser>();
services.AddScoped<IResponseParser<Dictionary<string, string>>, ProductDetailsResponseParser>();
services.AddScoped<IResponseParser<CustomFieldConfigs>, CustomFieldRequestResponseParser>();
services.AddScoped<IResponseParser<DealershipAuthRequest>, DealershipResponseParser>();
services.AddSingleton<UiSettings>();
// Add USPS scopes
services.AddScoped<IUspsVendorClient, USPS.Communication.UspsVendorClient>();
services.AddScoped<USPS.Communication.IUspsService, USPS.Communication.UspsService>();
services.AddScoped<USPS.Parsers.IResponseParser<Address>, USPS.Parsers.AddressValidationResponseParser>();
services.Configure<ApplicationControlConfiguration>(options => Configuration.GetSection("ApplicationControl").Bind(options));
services.Configure<CrifSettings>(options =>
Configuration.GetSection("CrifSettings").Bind(options));
services.Configure<BlobStorageSettings>(options =>
Configuration.GetSection("BlobStorageSettings").Bind(options));
services.Configure<BlobStorageConnectionSettings>(options =>
Configuration.GetSection("BlobStorageConnectionSettings").Bind(options));
services.Configure<DealershipEmailSettings>(options =>
Configuration.GetSection("DealershipEmailSettings").Bind(options));
services.AddSingleton<IBlobStorageAccess, BlobStorageAccess>();
// Set up USPS
services.Configure<USPS.Configuration.UspsSettings>(options =>
Configuration.GetSection("UspsSettings").Bind(options));
// Setup up Email
services.Configure<NotificationsApiConfiguration>(options =>
Configuration.GetSection("NotificationsApiConfiguration").Bind(options));
services.AddScoped<INotificationsApiService, NotificationsApiService>();
services.AddSingleton<IEmailTemplateService, EmailTemplateService>();
// Setup up Core
var coreSettings = new CoreSettings();
Configuration.GetSection("CoreSettings").Bind(coreSettings);
services.AddSingleton(coreSettings);
services.AddScoped<ICoreService, CoreService>();
services.AddScoped<IPdfService, PdfService>();
// Set up SAML IdP scopes
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ISAMLParser<DISAMLAssertion>, SAMLParser>();
// Set up SAML IdP configuration
services.Configure<OnlineBankingAuth.Configuration.SecuritySettings>(options =>
{
Configuration.GetSection("SecuritySettings").Bind(options);
});
// Setup DI service
services.AddScoped<IDigitalInsightVendorClient, DigitalInsightVendorClient>();
services.AddScoped<IDigitalInsightService, DigitalInsightService>();
services.AddScoped<IAuthTokenParser, AuthTokenParser>();
//Set up Authentication
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// AzureAD config
.AddAzureAdBearer(options => Configuration.Bind("AzureActiveDirectory", options));
services.AddAuthorization();
services.AddDistributedMemoryCache();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ApplicationSettings appSettings)
{
Log.Information("Configuring...");
var policyCollection = new HeaderPolicyCollection()
.AddDefaultSecurityHeaders()
.AddCustomHeader("Cache-Control", "no-cache")
.AddCustomHeader("Arr-Disable-Session-Affinity", "true");
app.UseCookiePolicy(new CookiePolicyOptions
{
CheckConsentNeeded = context => false,
MinimumSameSitePolicy = SameSiteMode.None
});
app.UseSecurityHeaders(policyCollection);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Landmark Loan API"));
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false)
app.UseCors("DevCors");
else
app.UseCors("ProdCors");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
}
@@ -0,0 +1,43 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
namespace Landmark.OLA.Application
{
public interface IStatus
{
string Application { get; }
DateTime CurrentDate { get; }
string BuildVersion { get; }
DateTime BuildDate { get; }
string Environment { get; }
}
public class Status : IStatus
{
public string Application => _env.ApplicationName;
public DateTime CurrentDate => DateTime.Now;
public string BuildVersion => Assembly.GetExecutingAssembly().GetName().Version.ToString();
public DateTime BuildDate => AssemblyBuildDate();
public string Environment => _env.EnvironmentName;
private readonly IWebHostEnvironment _env;
public Status(IWebHostEnvironment env)
{
_env = env;
}
public static DateTime AssemblyBuildDate()
{
//https://stackoverflow.com/a/804310/266594
//TODO #246: Look up this value at start up to avoid hitting the filesystem repeatedly
Assembly assembly = Assembly.GetExecutingAssembly();
System.IO.FileInfo fileInfo = new(assembly.Location);
DateTime lastModified = fileInfo.LastWriteTime;
return lastModified;
}
}
}
@@ -0,0 +1,158 @@
{
"Primary": {
"AppName": "Loan API",
"SeqUrl": "http://localhost:5080/",
"SeqKey": "VALUE-IN-KEYVAULT",
"LogFilePath": "serilog.log"
},
"AzureActiveDirectory": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "landmarkcu.com",
"TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499",
/** the azure id of the backend **/
"ClientId": "f8173aad-fccb-4226-8c6a-963693956ad4",
/** the azure id of the front end **/
"ValidAudience": "59d54f39-3ff9-4166-8b16-254c00416754"
},
"ApplicationControl": {
"NotifyLogging": true,
"HomePageUrl": "http://localhost:4200",
"ImportantTermsPdfFilePath": "./Files/IMPORTANT TERMS OF OUR HOME EQUITY LINE OF CREDIT.pdf",
"WhatYouShouldKnowPdfFilePath": "./Files/What You Should Know About Home Equity Lines of Credit Revised 8-2022.pdf"
},
"AllowedHosts": "*",
"UspsSettings": {
"Username": "VALUE-IN-KEYVAULT",
"Password": "No Password",
"EndpointUrl": "http://production.shippingapis.com/ShippingAPI.dll?"
},
"ApplicationStorage": {
"DatabaseName": "LoanApplications",
"UserName": "loan-app-dvl",
"Host": "loan-app-dvl.documents.azure.com",
"Port": 10255,
"Password": "VALUE-IN-KEYVAULT"
},
"CrifSettings": {
"ApiSettings": {
"Username": "VALUE-IN-KEYVAULT",
"InterfaceId": 600002,
"OrgCode": "LandmarkCU",
"OrgId": 4,
"EndpointUrl": "https://www-lc3cst.myappro.com/ACS/ConnectorService.svc/ConnectorService"
},
"SourceChannels": {
"DefaultSourceChannelId": 2399724,
"CreditCardSourceChannelId": 2399734
},
"ProductIds": {
"CC_CashBack": 132,
"CC_Rewards": 133,
"CC_Student": 134,
"CC_Secure": 136,
"EQ_HELOC_Flex": 139,
"EQ_Home_Equity_Fixed": 148,
"EQ_Mtg_Refi_Express": 149,
"EQ_Mfg_Home_And_Land": 146,
"V_Vehicle": 125,
"V_VehiclePreapproval": 126,
"M_Boat": 129,
"P_Powersport": 131,
"T_TrailerMotorhome": 130,
"S_Signature": 140
},
"ServicingChannels": {
"DefaultServicingChannel": 1698432
},
"CustomFieldMappings": {
"LoanTypeToPromoId": {
"132": "-114",
"133": "-114",
"134": "-114",
"136": "-114",
"139": "-118",
"148": "-118",
"149": "-118",
"146": "-118",
"125": "-117",
"126": "-117",
"129": "-117",
"131": "-117",
"130": "-117",
"140": "-115"
},
"CustomFieldID": {
"LicensePlateNumber": "-76",
"CountyTitled": "-96",
"MunicipalityType": "-173",
"MunicipalityWhereKept": "-174"
},
"MunicipalityTypeCode": {
"CITY": "402",
"TOWN": "403",
"VILLAGE": "404"
}
},
"FormatCategoryMappings": {
"Categories": {
"Stip Verification": "-4",
"Loan Application": "-3",
"Vehicle Value": "-2",
"Proof of Income": "-1",
"Attachment": "1",
"Appraisal": "12",
"Flood": "13",
"Title": "14",
"eDocs": "15",
"eCon": "16"
},
"DocumentToCategory": {
"Proof of Income": "Proof of Income",
"Boat Title": "Title",
"Trailer Title": "Title",
"Copy of the homeowners insurance": "Attachment",
"Proof of being a student": "Attachment",
"Copy of first mortgage balance statement": "Attachment",
"DNR Registration Card": "Attachment",
"Purchase Contract": "Attachment",
"Manufactured Home Title / Purchase Contract": "Attachment"
}
}
},
"SecuritySettings": {
"PrivateCertAzureThumbprint": "VALUE-IN-KEYVAULT",
"PublicCertAzureThumbprint": "VALUE-IN-KEYVAULT",
"TokenSecret": "VALUE-IN-KEYVAULT",
"SAMLSuccessRedirectRoute": "http://localhost:4200/select-loan?referrer=NCR",
"SAMLFailureRedirectRoute": "http://localhost:4200/error?referrer=NCR",
"DigitalInsightPostTokenURI": "https://diapis.stg1.digitalinsight.com/v1/oauth/token",
"DigitalInsightConsumerKey": "VALUE-IN-KEYVAULT",
"DigitalInsightConsumerSecret": "VALUE-IN-KEYVAULT",
"DigitalInsightFIID": "03456"
},
"NotificationsApiConfiguration": {
"Url": "http://localhost:50353",
"SendEmail": "/api/Email",
"ErrorRecipients": [ "[email protected]" ],
"RetryCount": 5
},
"CoreSettings": {
"EndPoint": "http://localhost:44355/",
"Timeout": 60
},
"BlobStorageSettings": {
"MaxFileSizeInMB": "10.0",
"AllowedFileTypes": [ "gif", "jpg", "jpeg", "png", "pdf" ],
"FileLimit": "5"
},
"BlobStorageConnectionSettings": {
"AccountName": "devloanappsa",
"AccountKey": "VALUE-IN-KEYVAULT"
},
"DealershipEmailSettings": {
"RedBannerMessage": "This is from the QA test environment.",
"Recipients": [ "[email protected]" ],
"ActionLink": "https://actiona6t.criflending.com/Core/Application/AppSecurity.aspx?orgCode=landmarkcu&resID=&resTypeID=-995&appID=",
"EmailAPIUrl": "https://localhost:50353/api/Email"
}
}