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,6 +34,42 @@ import { configRoutes } from './routes/v1/config.js'
|
||||
import { RbacService } from './services/rbac.service.js'
|
||||
import { ModuleService } from './services/module.service.js'
|
||||
import { AppConfigService } from './services/config.service.js'
|
||||
import { SettingsService } from './services/settings.service.js'
|
||||
import { users } from './db/schema/users.js'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
async function seedInitialUser(app: Awaited<ReturnType<typeof buildApp>>) {
|
||||
const email = process.env.INITIAL_USER_EMAIL
|
||||
const password = process.env.INITIAL_USER_PASSWORD
|
||||
const firstName = process.env.INITIAL_USER_FIRST_NAME
|
||||
const lastName = process.env.INITIAL_USER_LAST_NAME
|
||||
if (!email || !password || !firstName || !lastName) return
|
||||
|
||||
const existing = await app.db.select({ id: users.id }).from(users).limit(1)
|
||||
if (existing.length > 0) return
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10)
|
||||
await app.db.insert(users).values({ email, passwordHash, firstName, lastName, role: 'admin' })
|
||||
app.log.info({ email }, 'Initial admin user created')
|
||||
}
|
||||
|
||||
async function seedEmailSettings(app: Awaited<ReturnType<typeof buildApp>>) {
|
||||
const apiKey = process.env.RESEND_API_KEY
|
||||
if (!apiKey) return
|
||||
|
||||
const existing = await SettingsService.get(app.db, 'email.provider')
|
||||
if (existing) return
|
||||
|
||||
await SettingsService.set(app.db, 'email.provider', 'resend')
|
||||
await SettingsService.set(app.db, 'email.resend_api_key', apiKey, true)
|
||||
if (process.env.MAIL_FROM) {
|
||||
await SettingsService.set(app.db, 'email.from_address', process.env.MAIL_FROM)
|
||||
}
|
||||
if (process.env.BUSINESS_NAME) {
|
||||
await SettingsService.set(app.db, 'email.business_name', process.env.BUSINESS_NAME)
|
||||
}
|
||||
app.log.info('Email settings seeded from environment')
|
||||
}
|
||||
|
||||
export async function buildApp() {
|
||||
const app = Fastify({
|
||||
@@ -159,6 +195,16 @@ export async function buildApp() {
|
||||
} catch (err) {
|
||||
app.log.error({ err }, 'Failed to load app config')
|
||||
}
|
||||
try {
|
||||
await seedInitialUser(app)
|
||||
} catch (err) {
|
||||
app.log.error({ err }, 'Failed to seed initial user')
|
||||
}
|
||||
try {
|
||||
await seedEmailSettings(app)
|
||||
} catch (err) {
|
||||
app.log.error({ err }, 'Failed to seed email settings')
|
||||
}
|
||||
})
|
||||
|
||||
return app
|
||||
|
||||
Reference in New Issue
Block a user