// apps/api/src/services/databaseService.ts
import { Pool } from 'pg'
import crypto from 'crypto'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : undefined,
})
// Encryption helpers
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY // Must be 32 bytes
const ALGORITHM = 'aes-256-cbc'
function encrypt(text: string): { encrypted: string; iv: string } {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return { encrypted: encrypted.toString('hex'), iv: iv.toString('hex') }
}
function decrypt(encrypted: string, iv: string): string {
const decipher = crypto.createDecipheriv(
ALGORITHM,
Buffer.from(ENCRYPTION_KEY, 'hex'),
Buffer.from(iv, 'hex')
)
let decrypted = decipher.update(Buffer.from(encrypted, 'hex'))
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}
// Store Plaid access token (encrypted)
export async function storeAccessToken(
userId: string,
itemId: string,
accessToken: string,
institutionId: string,
institutionName: string
): Promise<void> {
const { encrypted, iv } = encrypt(accessToken)
await pool.query(
`INSERT INTO plaid_items (item_id, user_id, access_token_encrypted, access_token_iv, institution_id, institution_name)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (item_id) DO UPDATE
SET access_token_encrypted = $3, access_token_iv = $4, last_synced = NOW()`,
[itemId, userId, encrypted, iv, institutionId, institutionName]
)
}
// Retrieve Plaid access token (decrypted)
export async function getAccessToken(userId: string): Promise<string | null> {
const result = await pool.query(
`SELECT access_token_encrypted, access_token_iv FROM plaid_items WHERE user_id = $1 LIMIT 1`,
[userId]
)
if (result.rows.length === 0) return null
const { access_token_encrypted, access_token_iv } = result.rows[0]
return decrypt(access_token_encrypted, access_token_iv)
}
// Store financial profile
export async function storeFinancialProfile(
userId: string,
profile: FinancialProfile
): Promise<void> {
await pool.query(
`INSERT INTO financial_profiles (user_id, liquid_assets, credit_debt, loan_debt, monthly_income, monthly_spending, spending_by_category, spending_volatility)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (user_id) DO UPDATE
SET liquid_assets = $2, credit_debt = $3, loan_debt = $4, monthly_income = $5, monthly_spending = $6, spending_by_category = $7, spending_volatility = $8, updated_at = NOW()`,
[
userId,
profile.liquidAssets,
profile.creditDebt,
profile.loanDebt,
profile.monthlyIncome,
profile.monthlySpending,
JSON.stringify(profile.spendingByCategory),
profile.spendingVolatility,
]
)
}
// Save simulation result
export async function saveSimulationResult(
userId: string,
goal: Goal,
results: SimulationResults
): Promise<string> {
const result = await pool.query(
`INSERT INTO goals (user_id, goal_text, goal_type, target_amount, timeline_months, success_probability, median_outcome, simulation_results)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING goal_id`,
[
userId,
goal.goalText,
goal.goalType,
goal.targetAmount,
goal.timelineMonths,
results.successProbability,
results.medianOutcome,
JSON.stringify(results),
]
)
return result.rows[0].goal_id
}