feat: add app settings table, encryption utility, and generic email service
Some checks failed
Build & Release / build (push) Failing after 35s

- app_settings table with encrypted field support (AES-256-GCM, key from ENCRYPTION_KEY env)
- SettingsService for transparent encrypt/decrypt on get/set
- EmailService factory with Resend and SendGrid providers (SMTP stub) — provider config lives in app_settings
- Seeds initial admin user and email settings from env vars on first startup if not already present
- Migration 0039_app_settings.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ryan Moon
2026-04-05 10:26:07 -05:00
parent 81d37a2c68
commit b8e39369f1
8 changed files with 251 additions and 1 deletions

View File

@@ -0,0 +1,34 @@
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
const ALGORITHM = 'aes-256-gcm'
function getKey(): Buffer {
const hex = process.env.ENCRYPTION_KEY
if (!hex) throw new Error('ENCRYPTION_KEY env var is required for encrypted settings')
const key = Buffer.from(hex, 'hex')
if (key.length !== 32) throw new Error('ENCRYPTION_KEY must be a 64-character hex string (32 bytes)')
return key
}
export function encrypt(plaintext: string): { ciphertext: string; iv: string } {
const key = getKey()
const iv = randomBytes(12)
const cipher = createCipheriv(ALGORITHM, key, iv)
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag().toString('hex')
return {
ciphertext: `${encrypted}:${authTag}`,
iv: iv.toString('hex'),
}
}
export function decrypt(ciphertext: string, iv: string): string {
const key = getKey()
const [encrypted, authTag] = ciphertext.split(':')
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'hex'))
decipher.setAuthTag(Buffer.from(authTag, 'hex'))
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}