Finalized added puppeteer changes

This commit is contained in:
William Lewis 2024-04-04 11:03:38 -05:00
parent f32b07b2b8
commit 4c69e4989c
21 changed files with 120 additions and 745 deletions

View File

@ -1,42 +1,30 @@
namespace AAIntegration.SimmonsBank.API.Controllers; namespace AAIntegration.SimmonsBank.API.Controllers;
using AutoMapper;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using AAIntegration.SimmonsBank.API.Models.Accounts; using AAIntegration.SimmonsBank.API.Models.Accounts;
using AAIntegration.SimmonsBank.API.Services; using AAIntegration.SimmonsBank.API.Services;
using AAIntegration.SimmonsBank.API.Config;
using System.Collections.Generic; using System.Collections.Generic;
using AAIntegration.SimmonsBank.API.Entities; using AAIntegration.SimmonsBank.API.Entities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using AAIntegration.SimmonsBank.API.Models.Transactions;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
public class AccountsController : ControllerBase public class AccountsController : ControllerBase
{ {
private IAccountService _accountService;
private IUserService _userService; private IUserService _userService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
private readonly ILogger<AccountsController> _logger; private readonly ILogger<AccountsController> _logger;
private IPuppeteerService _puppeteerService; private IPuppeteerService _puppeteerService;
public AccountsController( public AccountsController(
IAccountService accountService,
IUserService userService, IUserService userService,
IMapper mapper,
IOptions<AppSettings> appSettings,
ILogger<AccountsController> logger, ILogger<AccountsController> logger,
IPuppeteerService puppeteerService) IPuppeteerService puppeteerService)
{ {
_accountService = accountService;
_userService = userService; _userService = userService;
_mapper = mapper;
_appSettings = appSettings.Value;
_logger = logger; _logger = logger;
_puppeteerService = puppeteerService; _puppeteerService = puppeteerService;
} }
@ -44,56 +32,35 @@ public class AccountsController : ControllerBase
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAllAsync() public async Task<IActionResult> GetAllAsync()
{ {
List<AccountDTO> accounts = await _puppeteerService.GetAccounts(_userService.GetUser(User.FindFirstValue(ClaimTypes.NameIdentifier))); List<AccountDTO> accounts = await _puppeteerService.GetAccounts(GetCurrentUser());
return Ok(accounts); return Ok(accounts);
/*List<AccountDTO> accountDtos = new List<AccountDTO>();
foreach (Account acc in _accountService.GetAll(GetCurrentUserId()))
accountDtos.Add(_mapper.Map<Account, AccountDTO>(acc));
return Ok(accountDtos);*/
} }
[HttpGet("{id}")] [HttpGet("{account_guid}")]
public IActionResult GetById(int id) public async Task<IActionResult> GetByGUIDAsync(string account_guid)
{ {
Account account = _accountService.GetById(id, GetCurrentUserId()); return Ok(await GetAccountAsync(account_guid));
return Ok(_mapper.Map<Account, AccountDTO>(account));
} }
[HttpPost] [HttpGet("{account_guid}/transactions")]
public IActionResult Create([FromBody]AccountCreateRequest model) public async Task<IActionResult> GetTransactionsAsync(string account_guid, uint offset = 0, uint limit = 500)
{ {
_accountService.Create(model, GetCurrentUserId()); List<TransactionDTO> transactions = await _puppeteerService.GetTransactions(GetCurrentUser(), (await GetAccountAsync(account_guid)).Id, offset, limit);
return Ok(new { message = "account created" }); return Ok(transactions);
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]AccountUpdateRequest model)
{
_accountService.Update(id, model, GetCurrentUserId());
return Ok(new { message = "account updated" });
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_accountService.Delete(id, GetCurrentUserId());
return Ok(new { message = "account deleted" });
} }
// Helpers // Helpers
private int GetCurrentUserId() private async Task<AccountDTO> GetAccountAsync(string account_guid)
{
List<AccountDTO> accounts = await _puppeteerService.GetAccounts(GetCurrentUser());
AccountDTO account = accounts.FirstOrDefault(a => a.Id == account_guid) ?? throw new KeyNotFoundException("Account not found");
return account;
}
private User GetCurrentUser()
{ {
string apiKey = User.FindFirstValue(ClaimTypes.NameIdentifier); string apiKey = User.FindFirstValue(ClaimTypes.NameIdentifier);
return _userService.GetUser(apiKey);
if (apiKey is null)
_logger.LogInformation($"ApiKey: is null");
_logger.LogInformation($"apiKey: {apiKey}");
return _userService.GetUser(apiKey).Id;
} }
} }

View File

@ -1,109 +0,0 @@
namespace AAIntegration.SimmonsBank.API.Controllers;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using AAIntegration.SimmonsBank.API.Models.Transactions;
using AAIntegration.SimmonsBank.API.Services;
using AAIntegration.SimmonsBank.API.Config;
using System.Runtime.InteropServices;
using AAIntegration.SimmonsBank.API.Entities;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
[Authorize]
[ApiController]
[Route("[controller]")]
public class TransactionsController : ControllerBase
{
private ITransactionService _transactionService;
private IUserService _userService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
private readonly ILogger<TransactionsController> _logger;
public TransactionsController(
ITransactionService transactionService,
IUserService userService,
IMapper mapper,
IOptions<AppSettings> appSettings,
ILogger<TransactionsController> logger)
{
_transactionService = transactionService;
_userService = userService;
_mapper = mapper;
_appSettings = appSettings.Value;
_logger = logger;
}
[HttpGet]
public IActionResult GetAll(int? accountId = null)
{
List<TransactionDto> transactionDtos = new List<TransactionDto>();
foreach (Transaction tran in _transactionService.GetAll(this.GetCurrentUserId()))
{
if (accountId.HasValue
&& (tran.DebitAccount == null || tran.DebitAccount.Id != accountId)
&& (tran.CreditAccount == null || tran.CreditAccount.Id != accountId))
continue;
transactionDtos.Add(_mapper.Map<Transaction, TransactionDto>(tran));
}
// Sort by Date
transactionDtos.Sort((t1, t2) => t2.Date.CompareTo(t1.Date));
return Ok(transactionDtos);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
Transaction tran = _transactionService.GetById(id, this.GetCurrentUserId());
return Ok(_mapper.Map<Transaction, TransactionDto>(tran));
}
[HttpPost("BulkAdd")]
public IActionResult BulkCreate([FromBody]List<TransactionCreate> model)
{
List<Transaction> trans = _transactionService.BulkCreate(model, this.GetCurrentUserId()).ToList();
return Ok(new { message = $"{trans.Count()} transaction(s) created." });
}
[HttpPost]
public IActionResult Create([FromBody]TransactionCreate model)
{
Transaction tran = _transactionService.Create(model, this.GetCurrentUserId());
return Ok(new { message = $"transaction '{tran.Description}' created with id '{tran.Id}'." });
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]TransactionUpdateRequest model)
{
_transactionService.Update(id, model, this.GetCurrentUserId());
return Ok(new { message = $"transaction with id '{id}' updated" });
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_transactionService.Delete(id, this.GetCurrentUserId());
return Ok(new { message = "transaction deleted" });
}
// Helpers
private int GetCurrentUserId()
{
string apiKey = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (apiKey is null)
_logger.LogInformation($"ApiKey: is null");
_logger.LogInformation($"apiKey: {apiKey}");
return _userService.GetUser(apiKey).Id;
}
}

View File

@ -58,13 +58,6 @@ public class UsersController : ControllerBase
private string GetCurrentUserApiKey() private string GetCurrentUserApiKey()
{ {
string apiKey = User.FindFirstValue(ClaimTypes.NameIdentifier); return User.FindFirstValue(ClaimTypes.NameIdentifier);
if (apiKey is null)
_logger.LogInformation($"ApiKey: is null");
_logger.LogInformation($"apiKey: {apiKey}");
return apiKey;
} }
} }

View File

@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace AAIntegration.SimmonsBank.API.Entities;
public class Account
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Balance { get; set; }
public string ExternalAccountNumber { get; set; }
public User Owner { get; set; }
}

View File

@ -1,19 +0,0 @@
using System.Collections.Generic;
using AAIntegration.SimmonsBank.API.Services;
namespace AAIntegration.SimmonsBank.API.Entities;
public class Transaction
{
public int Id { get; set; }
public DateTime Date { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
public string ExternalId { get; set; }
public string Description { get; set; }
public Account? DebitAccount { get; set; }
public Account? CreditAccount { get; set; }
public decimal Amount { get; set; }
public bool IsPending { get; set; }
public User Owner { get; set; }
}

View File

@ -3,14 +3,9 @@ namespace AAIntegration.SimmonsBank.API.Config;
using AutoMapper; using AutoMapper;
using AAIntegration.SimmonsBank.API.Entities; using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Models.Users; using AAIntegration.SimmonsBank.API.Models.Users;
using AAIntegration.SimmonsBank.API.Models.Accounts;
using AAIntegration.SimmonsBank.API.Services;
using System.Runtime.Serialization;
using AAIntegration.SimmonsBank.API.Models.Transactions;
public class AutoMapperProfile : Profile public class AutoMapperProfile : Profile
{ {
public AutoMapperProfile() public AutoMapperProfile()
{ // UserUpdateRequest -> User { // UserUpdateRequest -> User
CreateMap<UserUpdateRequest, User>() CreateMap<UserUpdateRequest, User>()
@ -26,55 +21,5 @@ public class AutoMapperProfile : Profile
return true; return true;
} }
)); ));
// AccountUpdateRequest -> Account
CreateMap<AccountUpdateRequest, Account>();
// AccountCreateRequest -> Account
CreateMap<AccountCreateRequest, Account>();
/*.ForMember(
dest => dest.OwnerId,
opt => opt.MapFrom(src => src.Owner)
);
/*.ForAllMembers(x => x.Condition(
(src, dest, prop) =>
{
// ignore both null & empty string properties
if (prop == null) return false;
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
return true;
}
))*/
// Account -> AccountGet
CreateMap<Account, AccountDTO>()
.ForAllMembers(x => x.Condition(
(src, dest, prop) =>
{
// ignore both null & empty string properties
if (prop == null) return false;
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
return true;
}
));
// Transaction -> TransactionDto
CreateMap<Transaction, TransactionDto>()
.ForMember(dest => dest.DebitAccountId, opt => opt.MapFrom(src => src.DebitAccount.Id))
.ForMember(dest => dest.CreditAccountId, opt => opt.MapFrom(src => src.CreditAccount.Id))
.ForAllMembers(x => x.Condition(
(src, dest, prop) =>
{
// ignore both null & empty string properties
if (prop == null) return false;
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
return true;
}
));
} }
} }

View File

@ -31,6 +31,4 @@ public class DataContext : DbContext
}*/ }*/
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public DbSet<Account> Accounts { get; set; }
public DbSet<Transaction> Transactions { get; set; }
} }

View File

@ -1,13 +0,0 @@
namespace AAIntegration.SimmonsBank.API.Models.Accounts;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using AAIntegration.SimmonsBank.API.Entities;
public class AccountCreateRequest
{
public string Name { get; set; }
public string InitialBalance { get; set; }
public int Currency { get; set; }
public string ExternalAccountNumber { get; set; }
}

View File

@ -12,4 +12,8 @@ public class AccountDTO
public string Numbers { get; set; } public string Numbers { get; set; }
public decimal? Balance { get; set; } public decimal? Balance { get; set; }
public decimal? AvailableBalance { get; set; } public decimal? AvailableBalance { get; set; }
public string AccountType { get; set; }
public string AccountSubType { get; set; }
public DateTime? PaymentDueDate { get; set; }
public decimal? PaymentDueAmount { get; set; }
} }

View File

@ -1,8 +0,0 @@
namespace AAIntegration.SimmonsBank.API.Models.Accounts;
public class AccountUpdateRequest
{
public string? Name { get; set; } = null;
public string? Balance { get; set; } = null;
public string? ExternalAccountNumber { get; set; } = null;
}

View File

@ -1,16 +0,0 @@
using System.Collections.Generic;
using System.Runtime;
using AAIntegration.SimmonsBank.API.Services;
namespace AAIntegration.SimmonsBank.API.Models.Transactions;
public class TransactionCreate
{
public DateTime Date { get; set; }
public string ExternalId { get; set; }
public string Description { get; set; }
public int? DebitAccount { get; set; }
public int? CreditAccount { get; set; }
public decimal Amount { get; set; }
public bool IsPending { get; set; }
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Runtime;
using AAIntegration.SimmonsBank.API.Services;
namespace AAIntegration.SimmonsBank.API.Models.Transactions;
public class TransactionDTO
{
public string Id { get; set; }
public string AccountId { get; set; }
public string Type { get; set; }
public decimal? Amount { get; set; }
public decimal? RunningBalance { get; set; }
public DateTime? DatePosted { get; set; }
public DateTime? Date { get; set; }
public DateTime? LastUpdated { get; set; }
public string PendingStatus { get; set; }
public string Memo { get; set; }
public string FilteredMemo { get; set; }
public string DisplayName { get; set; }
}

View File

@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Runtime;
using AAIntegration.SimmonsBank.API.Services;
namespace AAIntegration.SimmonsBank.API.Models.Transactions;
public class TransactionDto
{
public int Id { get; set; }
public DateTime Date { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
public string ExternalId { get; set; }
public string Description { get; set; }
public int DebitAccountId { get; set; }
public int CreditAccountId { get; set; }
public decimal Amount { get; set; }
public bool IsPending { get; set; }
}

View File

@ -1,15 +0,0 @@
using System.ComponentModel.DataAnnotations;
using AAIntegration.SimmonsBank.API.Entities;
namespace AAIntegration.SimmonsBank.API.Models.Transactions;
public class TransactionUpdateRequest
{
public DateTime? Date { get; set; } = null;
public string? ExternalId { get; set; } = null;
public string? Description { get; set; } = null;
public int? DebitAccount { get; set; } = null;
public int? CreditAccount { get; set; } = null;
public decimal? Amount { get; set; } = null;
public bool? IsPending { get; set; } = null;
}

View File

@ -46,16 +46,16 @@ public class PuppeteerProcess : IPuppeteerProcess
public async Task StayLoggedIn(User user) public async Task StayLoggedIn(User user)
{ {
_logger.LogInformation($"... doing work and processing for user {user.Id} ..."); string prefix = $"Task::StayLoggedIn - {user.Id} - ";
if (!await _puppeteerService.IsLoggedIn(user, _stoppingToken)) if (!await _puppeteerService.IsLoggedIn(user, _stoppingToken))
{ {
_logger.LogInformation("User determined to not be logged in"); _logger.LogInformation(prefix + "User is not logged in");
await _puppeteerService.Login(user, _stoppingToken); await _puppeteerService.Login(user, _stoppingToken);
} }
else else
{ {
_logger.LogInformation("User is already logged in"); _logger.LogInformation(prefix + "User is still logged in");
} }
} }

View File

@ -80,8 +80,6 @@ internal class Program
opt.UseNpgsql(dbConfig.GetConnectionString())); opt.UseNpgsql(dbConfig.GetConnectionString()));
builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddScoped<ITransactionService, TransactionService>();
builder.Services.AddScoped<ICacheService, CacheService>(); builder.Services.AddScoped<ICacheService, CacheService>();
builder.Services.AddScoped<IVersionService, VersionService>(); builder.Services.AddScoped<IVersionService, VersionService>();
builder.Services.AddScoped<IPuppeteerService, PuppeteerService>(); builder.Services.AddScoped<IPuppeteerService, PuppeteerService>();

View File

@ -1,132 +0,0 @@
namespace AAIntegration.SimmonsBank.API.Services;
using AutoMapper;
using BCrypt.Net;
using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Config;
using AAIntegration.SimmonsBank.API.Models.Accounts;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using Internal;
using Microsoft.EntityFrameworkCore;
public interface IAccountService
{
IEnumerable<Account> GetAll(int ownerId);
Account GetById(int accountId, int ownerId);
void Create(AccountCreateRequest model, int ownerId);
void Update(int accountId, AccountUpdateRequest model, int ownerId);
void Delete(int accountId, int ownerId);
}
public class AccountService : IAccountService
{
private DataContext _context;
private readonly IMapper _mapper;
private IUserService _userService;
public AccountService(
DataContext context,
IMapper mapper,
IUserService userService)
{
_context = context;
_mapper = mapper;
_userService = userService;
}
public IEnumerable<Account> GetAll(int ownerId)
{
return _context.Accounts
.Include(x => x.Owner)
.Where(x => x.Owner.Id == ownerId);
}
public Account GetById(int accountId, int ownerId)
{
return getAccount(accountId, ownerId);
}
public void Create(AccountCreateRequest model, int ownerId)
{
// Check that account with same name or same external number doesn't exist
IEnumerable<Account> accountsWithSameName = _context.Accounts
.Include(x => x.Owner)
.Where(x => x.Name.ToUpper() == model.Name.ToUpper() && x.Owner.Id == ownerId);
if (accountsWithSameName.Count() > 0)
throw new AppException("Account with name '" + model.Name + "' already exists");
if (!string.IsNullOrWhiteSpace(model.ExternalAccountNumber))
{
IEnumerable<Account> matches = _context.Accounts
.Include(x => x.Owner)
.Where(x => x.ExternalAccountNumber == model.ExternalAccountNumber && x.Owner.Id == ownerId);
if (matches.Count() > 0)
throw new AppException("Account with external account number '" + model.ExternalAccountNumber + "' already exists under account named '" + matches.First().Name + "'");
}
Account account = new Account {
Name = model.Name,
Balance = Convert.ToDecimal(model.InitialBalance),
ExternalAccountNumber = model.ExternalAccountNumber,
Owner = getOwner(ownerId)
};
_context.Accounts.Add(account);
_context.SaveChanges();
}
public void Update(int accountId, AccountUpdateRequest model, int ownerId)
{
Account account = getAccount(accountId, ownerId);
// validate
if (model.Name != account.Name && _context.Accounts
.Include(x => x.Owner)
.Any(x => x.Name == model.Name && x.Owner.Id == ownerId))
throw new AppException("Account with the name '" + model.Name + "' already exists");
// Name
if (!string.IsNullOrWhiteSpace(model.Name))
account.Name = model.Name;
// External Account Number
if (!string.IsNullOrWhiteSpace(model.ExternalAccountNumber))
account.ExternalAccountNumber = model.ExternalAccountNumber;
_context.Accounts.Update(account);
_context.SaveChanges();
}
public void Delete(int accountId, int ownerId)
{
var account = getAccount(accountId, ownerId);
_context.Accounts.Remove(account);
_context.SaveChanges();
}
// helper methods
private Account getAccount(int id, int ownerId)
{
var account = _context.Accounts
.Include(x => x.Owner)
.FirstOrDefault(x => x.Id == id && x.Owner.Id == ownerId);
if (account == null) throw new KeyNotFoundException("Account not found");
return account;
}
private User getOwner(int ownerId)
{
User? owner = _context.Users.Find(ownerId);
if (owner == null)
throw new AppException($"Owner with ID of '{ownerId}' could not be found");
return owner;
}
}

View File

@ -24,17 +24,19 @@ using NuGet.Protocol;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NuGet.Protocol.Core.Types; using NuGet.Protocol.Core.Types;
using AAIntegration.SimmonsBank.API.Models.Accounts; using AAIntegration.SimmonsBank.API.Models.Accounts;
using AAIntegration.SimmonsBank.API.Models.Transactions;
public interface IPuppeteerService public interface IPuppeteerService
{ {
Task<bool> Login(User user, CancellationToken cancellationToken); Task<bool> Login(User user, CancellationToken cancellationToken);
Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken); Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken);
Task<List<AccountDTO>> GetAccounts(User user); Task<List<AccountDTO>> GetAccounts(User user);
Task<List<TransactionDTO>> GetTransactions(User user, string accountGuid, uint offset = 0, uint limit = 500);
} }
public class PuppeteerService : IPuppeteerService public class PuppeteerService : IPuppeteerService
{ {
private const string API_BASE_PATH = "/a/consumer/api"; private const string API_BASE_PATH = "/a/consumer/api/v0";
private const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard"; private const string DASHBOARD_SELECTOR = "body > banno-web > bannoweb-layout > bannoweb-dashboard";
private const string USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; private const string USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
private readonly PuppeteerConfig _config; private readonly PuppeteerConfig _config;
@ -65,6 +67,7 @@ public class PuppeteerService : IPuppeteerService
public async Task<bool> Login(User user, CancellationToken cancellationToken) public async Task<bool> Login(User user, CancellationToken cancellationToken)
{ {
string prefix = $"Task::Login - {user.Id} - ";
TimeSpan timeout = TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds); TimeSpan timeout = TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds);
// Setup Page // Setup Page
@ -105,7 +108,7 @@ public class PuppeteerService : IPuppeteerService
} }
else else
{ {
_logger.LogError("Failed to find Sign-In button"); _logger.LogError(prefix + "Failed to find Sign-In button");
return false; return false;
} }
@ -131,7 +134,6 @@ public class PuppeteerService : IPuppeteerService
//IPage page = sender as IPage; //IPage page = sender as IPage;
page.Response -= LoginResponseHandler; page.Response -= LoginResponseHandler;
_logger.LogInformation("-----PARSING JSON-----");
JToken json = await args.Response.JsonAsync<JToken>(); JToken json = await args.Response.JsonAsync<JToken>();
string userId = json["id"].Value<string>(); string userId = json["id"].Value<string>();
_cacheService.SetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, userId); _cacheService.SetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, userId);
@ -147,7 +149,7 @@ public class PuppeteerService : IPuppeteerService
} }
else else
{ {
_logger.LogError("Failed to find Verify button"); _logger.LogError(prefix + "Failed to find Verify button");
return false; return false;
} }
@ -157,21 +159,22 @@ public class PuppeteerService : IPuppeteerService
} }
catch(TimeoutException) catch(TimeoutException)
{ {
_logger.LogWarning($"Dashboard isn't loading after login for user '{user.Id}'"); _logger.LogWarning(prefix + $"Dashboard isn't loading after login");
return false; return false;
} }
_logger.LogInformation($"Dashboard found for '{user.Id}'"); _logger.LogInformation(prefix + $"Login success");
return true;
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
_logger.LogError($"Login Task for user '{user.Id}' was canceled"); _logger.LogError(prefix + $"Login Task was canceled");
} }
catch (TimeoutException ex) catch (TimeoutException ex)
{ {
//_logger.LogWarning($"Login Task timed out for user '{user.Id}' after {timeout} seconds"); //_logger.LogWarning($"Login Task timed out for user '{user.Id}' after {timeout} seconds");
_logger.LogError(0, ex, $"Login Task timed out for user '{user.Id}' after {timeout} seconds"); _logger.LogError(0, ex, prefix + $"Login Task timed out after {timeout} seconds");
return false; return false;
} }
finally finally
@ -179,8 +182,7 @@ public class PuppeteerService : IPuppeteerService
await page.CloseAsync(); await page.CloseAsync();
} }
_logger.LogInformation($"Login completed for user {user.Id}"); return false;
return true;
} }
public async Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken) public async Task<bool> IsLoggedIn(User user, CancellationToken cancellationToken)
@ -191,7 +193,7 @@ public class PuppeteerService : IPuppeteerService
string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, ""); string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, "");
if (string.IsNullOrWhiteSpace(userSbId)) if (string.IsNullOrWhiteSpace(userSbId))
{ {
_logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in."); //_logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in.");
return false; return false;
} }
@ -207,22 +209,21 @@ public class PuppeteerService : IPuppeteerService
try try
{ {
IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken); IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds), cancellationToken);
_logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'"); //_logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'");
return response.Status == System.Net.HttpStatusCode.OK; return response.Status == System.Net.HttpStatusCode.OK;
} }
catch(TaskCanceledException) catch(TaskCanceledException)
{ {
_logger.LogWarning(prefix + $"Task was canceled"); _logger.LogError(prefix + $"Task was canceled");
} }
catch(TimeoutException) catch(TimeoutException)
{ {
_logger.LogWarning(prefix + $"Request to '{url}' timed out"); _logger.LogError(prefix + $"Request to '{url}' timed out");
} }
return false; return false;
} }
public async Task<List<AccountDTO>> GetAccounts(User user) public async Task<List<AccountDTO>> GetAccounts(User user)
{ {
string prefix = $"Task::GetAccounts - {user.Id} - "; string prefix = $"Task::GetAccounts - {user.Id} - ";
@ -231,7 +232,7 @@ public class PuppeteerService : IPuppeteerService
string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, ""); string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, "");
if (string.IsNullOrWhiteSpace(userSbId)) if (string.IsNullOrWhiteSpace(userSbId))
{ {
_logger.LogInformation(prefix + $"User SimmonsBank ID not found. User is not logged in."); _logger.LogWarning(prefix + $"User SimmonsBank ID not found. User is not logged in.");
return null; return null;
} }
@ -247,7 +248,7 @@ public class PuppeteerService : IPuppeteerService
try try
{ {
IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds)); IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds));
_logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'"); //_logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'");
if (response.Status == System.Net.HttpStatusCode.OK) if (response.Status == System.Net.HttpStatusCode.OK)
{ {
@ -259,11 +260,57 @@ public class PuppeteerService : IPuppeteerService
} }
catch(TaskCanceledException) catch(TaskCanceledException)
{ {
_logger.LogWarning(prefix + $"Task was canceled"); _logger.LogError(prefix + $"Task was canceled");
} }
catch(TimeoutException) catch(TimeoutException)
{ {
_logger.LogWarning(prefix + $"Request to '{url}' timed out"); _logger.LogError(prefix + $"Request to '{url}' timed out");
}
return null;
}
public async Task<List<TransactionDTO>> GetTransactions(User user, string accountGuid, uint offset = 0, uint limit = 500)
{
string prefix = $"Task::GetTransactions - {user.Id} - ";
// Get User ID
string userSbId = _cacheService.GetCachedUserValue<string>(user, PuppeteerConstants.USER_SB_ID, "");
if (string.IsNullOrWhiteSpace(userSbId))
{
_logger.LogWarning(prefix + $"User SimmonsBank ID not found. User is not logged in.");
return null;
}
// Setup Page
IBrowser browser = await GetUserBrowserAsync(user, new CancellationToken());
await using IPage page = await browser.NewPageAsync();
await page.SetUserAgentAsync(USER_AGENT);
await page.SetViewportAsync(new ViewPortOptions { Width = 1200, Height = 720 });
// Fetch transactions
string url = _config.SimmonsBankBaseUrl + API_BASE_PATH + $"/users/{userSbId}/accounts/{accountGuid}/transactions?offset={offset}&limit={limit}";
try
{
IResponse response = await page.GoToAsync(url).WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds));
//_logger.LogInformation(prefix + $"Request response code '{response.Status}'. Url: '{url}'");
if (response.Status == System.Net.HttpStatusCode.OK)
{
JToken transactions = await response.JsonAsync<JToken>();
return transactions.SelectToken("transactions").ToObject<List<TransactionDTO>>();
}
else
_logger.LogError(prefix + $"Received unexpected status code '{response.Status}'");
}
catch(TaskCanceledException)
{
_logger.LogError(prefix + $"Task was canceled");
}
catch(TimeoutException)
{
_logger.LogError(prefix + $"Request to '{url}' timed out");
} }
return null; return null;
@ -278,7 +325,7 @@ public class PuppeteerService : IPuppeteerService
if (cachedBrowser != null) if (cachedBrowser != null)
return cachedBrowser; return cachedBrowser;
_logger.LogInformation($"Could NOT find the browser for user with id '{user.Id}'. About to create one..."); //_logger.LogInformation($"Could NOT find the browser for user with id '{user.Id}'. About to create one...");
using var browserFetcher = new BrowserFetcher(); using var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync().WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds * 20), cancellationToken); await browserFetcher.DownloadAsync().WaitAsync(TimeSpan.FromSeconds(_config.BrowserOperationTimeoutSeconds * 20), cancellationToken);

View File

@ -1,230 +0,0 @@
namespace AAIntegration.SimmonsBank.API.Services;
using AutoMapper;
using BCrypt.Net;
using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Config;
using AAIntegration.SimmonsBank.API.Models.Transactions;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using Microsoft.EntityFrameworkCore;
using Internal;
using System.Collections.Immutable;
public interface ITransactionService
{
IEnumerable<Transaction> GetAll(int ownerId);
Transaction GetById(int id, int ownerId);
IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model, int ownerId);
Transaction Create(TransactionCreate model, int ownerId, bool errorOnFail = true);
void Update(int id, TransactionUpdateRequest model, int ownerId);
void Delete(int id, int ownerId);
}
public class TransactionService : ITransactionService
{
private DataContext _context;
private readonly IMapper _mapper;
private readonly ILogger<TransactionService> _logger;
public TransactionService(
DataContext context,
IMapper mapper,
ILogger<TransactionService> logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
}
public IEnumerable<Transaction> GetAll(int ownerId)
{
return _context.Transactions
.Include(t => t.DebitAccount)
.Include(t => t.CreditAccount)
.Include(t => t.Owner)
.Where(x => x.Owner.Id == ownerId);
}
public Transaction GetById(int id, int ownerId)
{
return getTransaction(id, ownerId);
}
private Account prepareAccount(int? accountId)
{
if (accountId == null || accountId.Value == 0)
{
return null;
}
Account account = _context.Accounts.Find(accountId.Value);
if (account == null)
throw new AppException("Could not find account with ID of '" + accountId.Value + "'.");
return account;
}
public IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model, int ownerId)
{
List<Transaction> transactions = new List<Transaction>();
foreach (TransactionCreate tr in model)
{
var tran = this.Create(tr, ownerId, false);
if (tran != null)
transactions.Add(tran);
}
return transactions;
}
public Transaction Create(TransactionCreate model, int ownerId, bool errorOnFail = true)
{
Transaction transaction = new Transaction {
Description = model.Description,
Date = model.Date.Date.ToUniversalTime(),
CreatedOn = DateTime.UtcNow,
UpdatedOn = DateTime.UtcNow,
ExternalId = string.IsNullOrWhiteSpace(model.ExternalId) ? "" : model.ExternalId,
DebitAccount = prepareAccount(model.DebitAccount),
CreditAccount = prepareAccount(model.CreditAccount),
Amount = Convert.ToDecimal(model.Amount),
Owner = this.getOwner(ownerId),
IsPending = model.IsPending
};
if (this.ValidateTransaction(transaction, ownerId, errorOnFail) == false)
{
_logger.LogInformation($"Aborted adding transaction '{transaction.Description}'.");
return null;
}
// At this point transaction itself is valid
_context.Transactions.Add(transaction);
_context.SaveChanges();
_logger.LogInformation("New transaction successfully created.");
return transaction;
}
public void Update(int id, TransactionUpdateRequest model, int ownerId)
{
Transaction transaction = getTransaction(id, ownerId);
// Transaction.Date
if (model.Date.HasValue)
transaction.Date = model.Date.Value;
// Transaction.ExternalId
if (model.ExternalId != null)
transaction.ExternalId = model.ExternalId;
// Transaction.Description
if (model.Description != null)
transaction.Description = model.Description;
// Transaction.DebitAccount
if (model.DebitAccount.HasValue)
transaction.DebitAccount = prepareAccount(model.DebitAccount);
// Transaction.CreditAccount
if (model.CreditAccount.HasValue)
transaction.CreditAccount = prepareAccount(model.CreditAccount.Value);
// Transaction.Amount
if (model.Amount.HasValue)
transaction.Amount = model.Amount.Value;
// Transaction.IsPending
if (model.IsPending.HasValue)
transaction.IsPending = model.IsPending.Value;
this.ValidateTransaction(transaction, ownerId);
transaction.UpdatedOn = DateTime.UtcNow;
_context.Transactions.Update(transaction);
_context.SaveChanges();
_logger.LogInformation($"Transaction '{id}' successfully updated.");
}
public void Delete(int id, int ownerId)
{
var transaction = getTransaction(id, ownerId);
_context.Transactions.Remove(transaction);
_context.SaveChanges();
}
// helpers
private bool ErrorOrFalse(bool error, string errorMessage)
{
if (error)
throw new AppException(errorMessage);
_logger.LogWarning(errorMessage);
return false;
}
private bool ValidateTransaction(Transaction transaction, int ownerId, bool errorOnFail = true)
{
// There has to be at least 1 specified account
if (transaction.DebitAccount == null && transaction.CreditAccount == null)
return ErrorOrFalse(errorOnFail, "There must be an envelope or account chosen for a transaction.");
// Accounts cannot be the same
if (transaction.DebitAccount != null && transaction.CreditAccount != null &&
transaction.DebitAccount.Id == transaction.CreditAccount.Id)
return ErrorOrFalse(errorOnFail, "The debit and credit accounts of a transaction cannot be the same.");
// Transaction Duplication Check - External ID
if (!string.IsNullOrWhiteSpace(transaction.ExternalId)
&& _context.Transactions
.Include(x => x.Owner)
.Any(x => x.ExternalId == transaction.ExternalId && x.Owner.Id == ownerId))
return ErrorOrFalse(errorOnFail, "Transaction with the external ID '" + transaction.ExternalId + "' already exists");
// Transaction Duplication Check - All other fields
/*if (_context.Transactions.Any(x =>
x.Description == transaction.Description
&& x.Date == transaction.Date
&& x.DebitAccount == transaction.DebitAccount
&& x.CreditAccount == transaction.CreditAccount
&& x.Amount == transaction.Amount))
{
return ErrorOrFalse(errorOnFail, "Transaction with the same fields already exists");
}*/
return true;
}
private Transaction getTransaction(int id, int ownerId)
{
var transaction = _context.Transactions
.Include(t => t.DebitAccount)
.Include(t => t.CreditAccount)
.Include(t => t.Owner)
.FirstOrDefault(t => t.Id == id && t.Owner.Id == ownerId);
if (transaction == null)
throw new KeyNotFoundException("Transaction not found");
return transaction;
}
private User getOwner(int ownerId)
{
User? owner = _context.Users.Find(ownerId);
if (owner == null)
throw new AppException($"Owner with ID of '{ownerId}' could not be found");
return owner;
}
}

View File

@ -1,59 +1,30 @@
namespace AAIntegration.SimmonsBank.API.Services; namespace AAIntegration.SimmonsBank.API.Services;
using AutoMapper;
using BCrypt.Net;
using AAIntegration.SimmonsBank.API.Entities; using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Config; using AAIntegration.SimmonsBank.API.Config;
using AAIntegration.SimmonsBank.API.Models.Users; using AAIntegration.SimmonsBank.API.Models.Users;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using System.Security.Cryptography; using System.Security.Cryptography;
public interface IUserService public interface IUserService
{ {
// New Based way
string Create(UserCreateRequest model); string Create(UserCreateRequest model);
void Update(string apiKey, UserUpdateRequest model); void Update(string apiKey, UserUpdateRequest model);
void Delete(string apiKey); void Delete(string apiKey);
Dictionary<string, int> GetAllApiKeys(); Dictionary<string, int> GetAllApiKeys();
User GetUser(string ApiKey); User GetUser(string ApiKey);
IEnumerable<User> GetAll(); IEnumerable<User> GetAll();
/* Other cringe way
AuthenticateResponse Authenticate(AuthenticateRequest model);
void Register(RegisterRequest model);
IEnumerable<User> GetAll();
User GetById(int id);
void Update(int id, UserUpdateRequest model);
void Delete(int id);
Dictionary<string, int> GetAllApiKeys();
string GetUserApiKey(int id);
void InvalidateApiKey(string apiKey);
string CreateUserApiKey(int id);
*/
} }
public class UserService : IUserService public class UserService : IUserService
{ {
private DataContext _context; private DataContext _context;
private readonly IMapper _mapper;
private readonly IOptions<AppSettings> _appSettings;
public UserService( public UserService(
DataContext context, DataContext context)
IMapper mapper,
IOptions<AppSettings> appSettings)
{ {
_context = context; _context = context;
_mapper = mapper;
_appSettings = appSettings;
} }
public string Create(UserCreateRequest model) public string Create(UserCreateRequest model)
@ -130,7 +101,7 @@ public class UserService : IUserService
return user; return user;
} }
private const string _prefix = "CT-"; private const string _prefix = "SB-";
private const int _numberOfSecureBytesToGenerate = 32; private const int _numberOfSecureBytesToGenerate = 32;
private const int _lengthOfKey = 32; private const int _lengthOfKey = 32;

View File

@ -4,6 +4,10 @@ This is an integration for ActiveAllocator.
The type is Transaction Importer, specifically created for interfacing with SimmonsBank's online banking website. The type is Transaction Importer, specifically created for interfacing with SimmonsBank's online banking website.
## Guides
[Pass parameters to HTTP GET action](https://code-maze.com/aspnetcore-pass-parameters-to-http-get-action/)
## Dependencies ## Dependencies
[Otp.NET library](https://github.com/kspearrin/Otp.NET) [Otp.NET library](https://github.com/kspearrin/Otp.NET)