feat: add app settings table, encryption utility, and generic email service
Some checks failed
Build & Release / build (push) Failing after 35s
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:
34
packages/backend/src/utils/encryption.ts
Normal file
34
packages/backend/src/utils/encryption.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user