fix: batch 4 — connection pooling, graceful shutdown, email verification UX
- Prisma: connection_limit=10, pool_timeout=30 on DATABASE_URL in both compose files - Graceful shutdown: SIGTERM/SIGINT forwarded to Node process in docker-entrypoint.sh - testEmailConnection: replaced real email send with transporter.verify(), simplified UI to single button - NotificationLog.userId index: confirmed already present, no change needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ../.env
|
- ../.env
|
||||||
environment:
|
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_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-dev-secret-key-for-local-development-only}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-dev-secret-key-for-local-development-only}
|
||||||
- AUTH_SECRET=${AUTH_SECRET:-dev-secret-key-for-local-development-only}
|
- AUTH_SECRET=${AUTH_SECRET:-dev-secret-key-for-local-development-only}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- 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_URL=${NEXTAUTH_URL}
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
- AUTH_SECRET=${NEXTAUTH_SECRET}
|
- AUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
|
|||||||
@@ -59,4 +59,18 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Starting application..."
|
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"
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
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")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -18,15 +17,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
smtp_host: z.string().min(1, 'SMTP host is required'),
|
smtp_host: z.string().min(1, 'SMTP host is required'),
|
||||||
@@ -51,8 +41,6 @@ interface EmailSettingsFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
||||||
const [testDialogOpen, setTestDialogOpen] = useState(false)
|
|
||||||
const [testEmail, setTestEmail] = useState('')
|
|
||||||
const utils = trpc.useUtils()
|
const utils = trpc.useUtils()
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
@@ -77,17 +65,16 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const sendTestEmail = trpc.settings.testEmailConnection.useMutation({
|
const verifyConnection = trpc.settings.testEmailConnection.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
setTestDialogOpen(false)
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Test email sent successfully')
|
toast.success('SMTP connection verified successfully')
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Failed to send test email: ${result.error}`)
|
toast.error(`SMTP verification failed: ${result.error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (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 })
|
updateSettings.mutate({ settings: settingsToUpdate })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSendTest = () => {
|
const handleVerifyConnection = () => {
|
||||||
if (!testEmail) {
|
verifyConnection.mutate()
|
||||||
toast.error('Please enter an email address')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendTestEmail.mutate({ testEmail })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -243,49 +226,24 @@ export function EmailSettingsForm({ settings }: EmailSettingsFormProps) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog open={testDialogOpen} onOpenChange={setTestDialogOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button type="button" variant="outline">
|
|
||||||
<Send className="mr-2 h-4 w-4" />
|
|
||||||
Send Test Email
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Send Test Email</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Enter an email address to receive a test email
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="test@example.com"
|
|
||||||
value={testEmail}
|
|
||||||
onChange={(e) => setTestEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setTestDialogOpen(false)}
|
onClick={handleVerifyConnection}
|
||||||
|
disabled={verifyConnection.isPending}
|
||||||
>
|
>
|
||||||
Cancel
|
{verifyConnection.isPending ? (
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSendTest}
|
|
||||||
disabled={sendTestEmail.isPending}
|
|
||||||
>
|
|
||||||
{sendTestEmail.isPending ? (
|
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Sending...
|
Verifying...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Send Test'
|
<>
|
||||||
|
<Send className="mr-2 h-4 w-4" />
|
||||||
|
Verify Connection
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -322,12 +322,11 @@ export const settingsRouter = router({
|
|||||||
* Test email connection
|
* Test email connection
|
||||||
*/
|
*/
|
||||||
testEmailConnection: superAdminProcedure
|
testEmailConnection: superAdminProcedure
|
||||||
.input(z.object({ testEmail: z.string().email() }))
|
.mutation(async () => {
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
try {
|
try {
|
||||||
const { sendTestEmail } = await import('@/lib/email')
|
const { verifyEmailConnection } = await import('@/lib/email')
|
||||||
const success = await sendTestEmail(input.testEmail)
|
const success = await verifyEmailConnection()
|
||||||
return { success, error: success ? null : 'Failed to send test email' }
|
return { success, error: success ? null : 'SMTP connection verification failed' }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||||
return { success: false, error: `Email configuration error: ${message}` }
|
return { success: false, error: `Email configuration error: ${message}` }
|
||||||
|
|||||||
Reference in New Issue
Block a user