Implement Phase 6: automated bank sync (Plaid + SimpleFIN) & settings
Add automated transaction syncing via Plaid and SimpleFIN providers, a Settings page for managing connections and account mappings, background scheduled syncing, and sync history logging. New backend: BankSync plugin project with PlaidSyncProvider and SimpleFinSyncProvider, SyncConnection/LinkedAccount/SyncLog models, AES-256 encryption service, BankSyncService orchestration, BankSyncBackgroundService (15-min tick), and BankSyncController (13 endpoints). EF migration creates sync_connections, linked_accounts, and sync_logs tables. New frontend: Settings view with 3 tabs (Profile, Bank Connections, Sync History), bankSync API service, BankSyncComplete SignalR notifications, and Settings nav item in sidebar + avatar dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Purrse.Core.Interfaces.Services;
|
||||
|
||||
namespace Purrse.Api.Services;
|
||||
|
||||
public class EncryptionService : IEncryptionService
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
|
||||
public EncryptionService(IConfiguration configuration)
|
||||
{
|
||||
var keyString = configuration["BankSync:EncryptionKey"]
|
||||
?? throw new InvalidOperationException("BankSync:EncryptionKey is not configured");
|
||||
_key = SHA256.HashData(Encoding.UTF8.GetBytes(keyString));
|
||||
}
|
||||
|
||||
public string Encrypt(string plainText)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
aes.GenerateIV();
|
||||
|
||||
using var encryptor = aes.CreateEncryptor();
|
||||
var plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
var result = new byte[aes.IV.Length + cipherBytes.Length];
|
||||
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
|
||||
Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length);
|
||||
|
||||
return Convert.ToBase64String(result);
|
||||
}
|
||||
|
||||
public string Decrypt(string cipherText)
|
||||
{
|
||||
var fullCipher = Convert.FromBase64String(cipherText);
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = _key;
|
||||
|
||||
var iv = new byte[aes.BlockSize / 8];
|
||||
var cipher = new byte[fullCipher.Length - iv.Length];
|
||||
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
|
||||
Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
|
||||
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
var plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user