diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 83d9c95..bd32c9e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -68,7 +68,7 @@ services: env_file: - ../.env environment: - - DATABASE_URL=postgresql://${POSTGRES_USER:-mopc}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-mopc} + - DATABASE_URL=postgresql://${POSTGRES_USER:-mopc}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-mopc}?connection_limit=10&pool_timeout=30 - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-dev-secret-key-for-local-development-only} - AUTH_SECRET=${AUTH_SECRET:-dev-secret-key-for-local-development-only} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e584d1e..81bba41 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ services: - .env environment: - NODE_ENV=production - - DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc + - DATABASE_URL=postgresql://mopc:${DB_PASSWORD}@postgres:5432/mopc?connection_limit=10&pool_timeout=30 - NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - AUTH_SECRET=${NEXTAUTH_SECRET} diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index f01577b..47f4d5b 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -59,4 +59,18 @@ else fi echo "==> Starting application..." -exec node server.js + +# Graceful shutdown: forward SIGTERM/SIGINT to the Node process +# so in-flight requests can complete before the container exits. +shutdown() { + echo "==> Received shutdown signal, stopping gracefully..." + kill -TERM "$NODE_PID" 2>/dev/null + wait "$NODE_PID" + exit $? +} + +trap shutdown TERM INT + +node server.js & +NODE_PID=$! +wait "$NODE_PID" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index db82af3..a4f36a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,6 +11,10 @@ generator client { datasource db { provider = "postgresql" + // connection_limit and pool_timeout are set via query params in DATABASE_URL: + // ?connection_limit=10&pool_timeout=30 + // Defaults: connection_limit = num_cpus * 2 + 1, pool_timeout = 10s. + // Override in .env for production to prevent connection exhaustion. url = env("DATABASE_URL") } diff --git a/src/components/settings/email-settings-form.tsx b/src/components/settings/email-settings-form.tsx index ae5aec7..7e5dbed 100644 --- a/src/components/settings/email-settings-form.tsx +++ b/src/components/settings/email-settings-form.tsx @@ -1,6 +1,5 @@ 'use client' -import { useState } from 'react' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' @@ -18,15 +17,6 @@ import { FormLabel, FormMessage, } from '@/components/ui/form' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog' const formSchema = z.object({ smtp_host: z.string().min(1, 'SMTP host is required'), @@ -51,8 +41,6 @@ interface EmailSettingsFormProps { } export function EmailSettingsForm({ settings }: EmailSettingsFormProps) { - const [testDialogOpen, setTestDialogOpen] = useState(false) - const [testEmail, setTestEmail] = useState('') const utils = trpc.useUtils() const form = useForm({ @@ -77,17 +65,16 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) { }, }) - const sendTestEmail = trpc.settings.testEmailConnection.useMutation({ + const verifyConnection = trpc.settings.testEmailConnection.useMutation({ onSuccess: (result) => { - setTestDialogOpen(false) if (result.success) { - toast.success('Test email sent successfully') + toast.success('SMTP connection verified successfully') } else { - toast.error(`Failed to send test email: ${result.error}`) + toast.error(`SMTP verification failed: ${result.error}`) } }, onError: (error) => { - toast.error(`Test failed: ${error.message}`) + toast.error(`Verification failed: ${error.message}`) }, }) @@ -107,12 +94,8 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) { updateSettings.mutate({ settings: settingsToUpdate }) } - const handleSendTest = () => { - if (!testEmail) { - toast.error('Please enter an email address') - return - } - sendTestEmail.mutate({ testEmail }) + const handleVerifyConnection = () => { + verifyConnection.mutate() } return ( @@ -243,49 +226,24 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) { )} - - - - - - - Send Test Email - - Enter an email address to receive a test email - - - setTestEmail(e.target.value)} - /> - - - - - - + Verify Connection + + )} + diff --git a/src/server/routers/settings.ts b/src/server/routers/settings.ts index 5c6e80b..10ab201 100644 --- a/src/server/routers/settings.ts +++ b/src/server/routers/settings.ts @@ -322,12 +322,11 @@ export const settingsRouter = router({ * Test email connection */ testEmailConnection: superAdminProcedure - .input(z.object({ testEmail: z.string().email() })) - .mutation(async ({ ctx, input }) => { + .mutation(async () => { try { - const { sendTestEmail } = await import('@/lib/email') - const success = await sendTestEmail(input.testEmail) - return { success, error: success ? null : 'Failed to send test email' } + const { verifyEmailConnection } = await import('@/lib/email') + const success = await verifyEmailConnection() + return { success, error: success ? null : 'SMTP connection verification failed' } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' return { success: false, error: `Email configuration error: ${message}` }