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:
106
packages/backend/src/services/email.service.ts
Normal file
106
packages/backend/src/services/email.service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { SettingsService } from './settings.service.js'
|
||||
|
||||
interface SendOpts {
|
||||
to: string
|
||||
subject: string
|
||||
html: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
interface EmailProvider {
|
||||
send(opts: SendOpts): Promise<void>
|
||||
}
|
||||
|
||||
class ResendProvider implements EmailProvider {
|
||||
constructor(private db: PostgresJsDatabase<any>) {}
|
||||
|
||||
async send(opts: SendOpts): Promise<void> {
|
||||
const apiKey = await SettingsService.get(this.db, 'email.resend_api_key')
|
||||
?? process.env.RESEND_API_KEY
|
||||
const from = await SettingsService.get(this.db, 'email.from_address')
|
||||
?? process.env.MAIL_FROM
|
||||
|
||||
if (!apiKey) throw new Error('Resend API key not configured')
|
||||
if (!from) throw new Error('email.from_address not configured')
|
||||
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from,
|
||||
to: opts.to,
|
||||
subject: opts.subject,
|
||||
html: opts.html,
|
||||
...(opts.text ? { text: opts.text } : {}),
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`Resend API error ${res.status}: ${body}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SendGridProvider implements EmailProvider {
|
||||
constructor(private db: PostgresJsDatabase<any>) {}
|
||||
|
||||
async send(opts: SendOpts): Promise<void> {
|
||||
const apiKey = await SettingsService.get(this.db, 'email.sendgrid_api_key')
|
||||
const from = await SettingsService.get(this.db, 'email.from_address')
|
||||
?? process.env.MAIL_FROM
|
||||
|
||||
if (!apiKey) throw new Error('SendGrid API key not configured')
|
||||
if (!from) throw new Error('email.from_address not configured')
|
||||
|
||||
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
personalizations: [{ to: [{ email: opts.to }] }],
|
||||
from: { email: from },
|
||||
subject: opts.subject,
|
||||
content: [
|
||||
{ type: 'text/html', value: opts.html },
|
||||
...(opts.text ? [{ type: 'text/plain', value: opts.text }] : []),
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`SendGrid API error ${res.status}: ${body}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SmtpProvider implements EmailProvider {
|
||||
async send(_opts: SendOpts): Promise<void> {
|
||||
throw new Error('SMTP email provider is not yet implemented')
|
||||
}
|
||||
}
|
||||
|
||||
export const EmailService = {
|
||||
async send(db: PostgresJsDatabase<any>, opts: SendOpts): Promise<void> {
|
||||
const provider = await SettingsService.get(db, 'email.provider')
|
||||
?? process.env.MAIL_PROVIDER
|
||||
|
||||
switch (provider) {
|
||||
case 'resend':
|
||||
return new ResendProvider(db).send(opts)
|
||||
case 'sendgrid':
|
||||
return new SendGridProvider(db).send(opts)
|
||||
case 'smtp':
|
||||
return new SmtpProvider().send(opts)
|
||||
default:
|
||||
throw new Error('Email provider not configured. Set email.provider in app_settings.')
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user