admin-settings.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. 'use client'
  2. import { useState, useTransition } from 'react'
  3. import { useRouter } from 'next/navigation'
  4. import { toast } from 'sonner'
  5. import { Input } from '@/components/ui/input'
  6. import { Button } from '@/components/ui/button'
  7. import { Label } from '@/components/ui/label'
  8. import { Switch } from '@/components/ui/switch'
  9. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
  10. import { Loader2, Send } from 'lucide-react'
  11. import { SYSTEM_SETTING_KEYS } from '../Schema/systemSettingsSchema'
  12. import type { SystemSettingsMap } from '../Schema/systemSettingsSchema'
  13. import { setSystemSettings } from '../Actions/setSystemSettings'
  14. import { testEmailConnection } from '../Actions/testEmailConnection'
  15. type EmailProviderType = 'smtp' | 'resend' | 'postmark' | 'mailgun' | 'sendgrid' | 'ses'
  16. export function AdminSettings({
  17. initial,
  18. }: {
  19. initial: SystemSettingsMap
  20. mode: 'cloud' | 'self-hosted'
  21. }) {
  22. const router = useRouter()
  23. const [isPending, startTransition] = useTransition()
  24. const [isTesting, setIsTesting] = useState(false)
  25. // Platform settings
  26. const [registrationDisabled, setRegistrationDisabled] = useState(
  27. initial[SYSTEM_SETTING_KEYS.REGISTRATION_DISABLED] === 'true'
  28. )
  29. // Email provider
  30. const [emailProvider, setEmailProvider] = useState<EmailProviderType>(
  31. (initial[SYSTEM_SETTING_KEYS.EMAIL_PROVIDER] as EmailProviderType) || 'smtp'
  32. )
  33. // SMTP settings
  34. const [smtpHost, setSmtpHost] = useState(initial[SYSTEM_SETTING_KEYS.SMTP_HOST] || '')
  35. const [smtpPort, setSmtpPort] = useState(initial[SYSTEM_SETTING_KEYS.SMTP_PORT] || '587')
  36. const [smtpUser, setSmtpUser] = useState(initial[SYSTEM_SETTING_KEYS.SMTP_USER] || '')
  37. const [smtpPass, setSmtpPass] = useState(initial[SYSTEM_SETTING_KEYS.SMTP_PASS] || '')
  38. const [smtpSecure, setSmtpSecure] = useState(initial[SYSTEM_SETTING_KEYS.SMTP_SECURE] === 'true')
  39. const [smtpFromEmail, setSmtpFromEmail] = useState(
  40. initial[SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL] || ''
  41. )
  42. const [smtpFromName, setSmtpFromName] = useState(
  43. initial[SYSTEM_SETTING_KEYS.SMTP_FROM_NAME] || ''
  44. )
  45. const [smtpRejectUnauthorized, setSmtpRejectUnauthorized] = useState(
  46. initial[SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED] !== 'false'
  47. )
  48. const [smtpRequireTls, setSmtpRequireTls] = useState(
  49. initial[SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS] === 'true'
  50. )
  51. // Resend settings
  52. const [resendApiKey, setResendApiKey] = useState(
  53. initial[SYSTEM_SETTING_KEYS.RESEND_API_KEY] || ''
  54. )
  55. const [resendFromEmail, setResendFromEmail] = useState(
  56. initial[SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL] || ''
  57. )
  58. const [resendFromName, setResendFromName] = useState(
  59. initial[SYSTEM_SETTING_KEYS.RESEND_FROM_NAME] || ''
  60. )
  61. // Postmark settings
  62. const [postmarkApiKey, setPostmarkApiKey] = useState(
  63. initial[SYSTEM_SETTING_KEYS.POSTMARK_API_KEY] || ''
  64. )
  65. const [postmarkFromEmail, setPostmarkFromEmail] = useState(
  66. initial[SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL] || ''
  67. )
  68. const [postmarkFromName, setPostmarkFromName] = useState(
  69. initial[SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME] || ''
  70. )
  71. // Mailgun settings
  72. const [mailgunApiKey, setMailgunApiKey] = useState(
  73. initial[SYSTEM_SETTING_KEYS.MAILGUN_API_KEY] || ''
  74. )
  75. const [mailgunDomain, setMailgunDomain] = useState(
  76. initial[SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN] || ''
  77. )
  78. const [mailgunRegion, setMailgunRegion] = useState(
  79. initial[SYSTEM_SETTING_KEYS.MAILGUN_REGION] || 'us'
  80. )
  81. const [mailgunFromEmail, setMailgunFromEmail] = useState(
  82. initial[SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL] || ''
  83. )
  84. const [mailgunFromName, setMailgunFromName] = useState(
  85. initial[SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME] || ''
  86. )
  87. // SendGrid settings
  88. const [sendgridApiKey, setSendgridApiKey] = useState(
  89. initial[SYSTEM_SETTING_KEYS.SENDGRID_API_KEY] || ''
  90. )
  91. const [sendgridFromEmail, setSendgridFromEmail] = useState(
  92. initial[SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL] || ''
  93. )
  94. const [sendgridFromName, setSendgridFromName] = useState(
  95. initial[SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME] || ''
  96. )
  97. // Amazon SES settings
  98. const [sesAccessKeyId, setSesAccessKeyId] = useState(
  99. initial[SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID] || ''
  100. )
  101. const [sesSecretAccessKey, setSesSecretAccessKey] = useState(
  102. initial[SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY] || ''
  103. )
  104. const [sesRegion, setSesRegion] = useState(
  105. initial[SYSTEM_SETTING_KEYS.SES_REGION] || 'us-east-1'
  106. )
  107. const [sesFromEmail, setSesFromEmail] = useState(
  108. initial[SYSTEM_SETTING_KEYS.SES_FROM_EMAIL] || ''
  109. )
  110. const [sesFromName, setSesFromName] = useState(
  111. initial[SYSTEM_SETTING_KEYS.SES_FROM_NAME] || ''
  112. )
  113. const handleSave = () => {
  114. startTransition(async () => {
  115. const data: Record<string, string> = {
  116. [SYSTEM_SETTING_KEYS.REGISTRATION_DISABLED]: String(registrationDisabled),
  117. [SYSTEM_SETTING_KEYS.EMAIL_PROVIDER]: emailProvider,
  118. // SMTP
  119. [SYSTEM_SETTING_KEYS.SMTP_HOST]: smtpHost,
  120. [SYSTEM_SETTING_KEYS.SMTP_PORT]: smtpPort,
  121. [SYSTEM_SETTING_KEYS.SMTP_USER]: smtpUser,
  122. [SYSTEM_SETTING_KEYS.SMTP_PASS]: smtpPass,
  123. [SYSTEM_SETTING_KEYS.SMTP_SECURE]: String(smtpSecure),
  124. [SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL]: smtpFromEmail,
  125. [SYSTEM_SETTING_KEYS.SMTP_FROM_NAME]: smtpFromName,
  126. [SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED]: String(smtpRejectUnauthorized),
  127. [SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS]: String(smtpRequireTls),
  128. // Resend
  129. [SYSTEM_SETTING_KEYS.RESEND_API_KEY]: resendApiKey,
  130. [SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL]: resendFromEmail,
  131. [SYSTEM_SETTING_KEYS.RESEND_FROM_NAME]: resendFromName,
  132. // Postmark
  133. [SYSTEM_SETTING_KEYS.POSTMARK_API_KEY]: postmarkApiKey,
  134. [SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL]: postmarkFromEmail,
  135. [SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME]: postmarkFromName,
  136. // Mailgun
  137. [SYSTEM_SETTING_KEYS.MAILGUN_API_KEY]: mailgunApiKey,
  138. [SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN]: mailgunDomain,
  139. [SYSTEM_SETTING_KEYS.MAILGUN_REGION]: mailgunRegion,
  140. [SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL]: mailgunFromEmail,
  141. [SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME]: mailgunFromName,
  142. // SendGrid
  143. [SYSTEM_SETTING_KEYS.SENDGRID_API_KEY]: sendgridApiKey,
  144. [SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL]: sendgridFromEmail,
  145. [SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME]: sendgridFromName,
  146. // Amazon SES
  147. [SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID]: sesAccessKeyId,
  148. [SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY]: sesSecretAccessKey,
  149. [SYSTEM_SETTING_KEYS.SES_REGION]: sesRegion,
  150. [SYSTEM_SETTING_KEYS.SES_FROM_EMAIL]: sesFromEmail,
  151. [SYSTEM_SETTING_KEYS.SES_FROM_NAME]: sesFromName,
  152. }
  153. const result = await setSystemSettings(data)
  154. if (result.success) {
  155. toast.success('Settings saved successfully')
  156. router.refresh()
  157. } else {
  158. toast.error(result.error ?? 'Failed to save settings')
  159. }
  160. })
  161. }
  162. const handleTestEmail = async () => {
  163. setIsTesting(true)
  164. try {
  165. const result = await testEmailConnection()
  166. if (result.success) {
  167. toast.success(`Test email sent to ${result.data?.sentTo}`)
  168. } else {
  169. toast.error(result.error ?? 'Email test failed')
  170. }
  171. } finally {
  172. setIsTesting(false)
  173. }
  174. }
  175. const isTestDisabled =
  176. isTesting ||
  177. (emailProvider === 'smtp' && !smtpHost) ||
  178. (emailProvider === 'resend' && !resendApiKey) ||
  179. (emailProvider === 'postmark' && !postmarkApiKey) ||
  180. (emailProvider === 'mailgun' && !mailgunApiKey) ||
  181. (emailProvider === 'sendgrid' && !sendgridApiKey) ||
  182. (emailProvider === 'ses' && !sesAccessKeyId)
  183. return (
  184. <div className="space-y-6">
  185. {/* Platform Settings */}
  186. <Card>
  187. <CardHeader>
  188. <CardTitle>Platform Settings</CardTitle>
  189. <CardDescription>General platform configuration options</CardDescription>
  190. </CardHeader>
  191. <CardContent className="space-y-6">
  192. <div className="flex items-center justify-between">
  193. <div className="space-y-0.5">
  194. <Label htmlFor="registration-toggle">Disable Registration</Label>
  195. <p className="text-xs text-muted-foreground">
  196. Block new user sign-ups on the platform
  197. </p>
  198. </div>
  199. <Switch
  200. id="registration-toggle"
  201. checked={registrationDisabled}
  202. onCheckedChange={setRegistrationDisabled}
  203. />
  204. </div>
  205. </CardContent>
  206. </Card>
  207. {/* Email Settings */}
  208. <Card>
  209. <CardHeader>
  210. <CardTitle>Email Settings</CardTitle>
  211. <CardDescription>
  212. Platform Email Provider — Used for system-level emails such as password resets and team invitations. This is also the default email provider for all organizations. Organizations can optionally configure their own email provider in Settings &gt; Email.
  213. </CardDescription>
  214. </CardHeader>
  215. <CardContent className="space-y-6">
  216. <div className="flex flex-wrap gap-2">
  217. <Button
  218. type="button"
  219. variant={emailProvider === 'smtp' ? 'default' : 'outline'}
  220. onClick={() => setEmailProvider('smtp')}
  221. className="flex-1"
  222. >
  223. SMTP
  224. </Button>
  225. <Button
  226. type="button"
  227. variant={emailProvider === 'resend' ? 'default' : 'outline'}
  228. onClick={() => setEmailProvider('resend')}
  229. className="flex-1"
  230. >
  231. Resend
  232. </Button>
  233. <Button
  234. type="button"
  235. variant={emailProvider === 'postmark' ? 'default' : 'outline'}
  236. onClick={() => setEmailProvider('postmark')}
  237. className="flex-1"
  238. >
  239. Postmark
  240. </Button>
  241. <Button
  242. type="button"
  243. variant={emailProvider === 'mailgun' ? 'default' : 'outline'}
  244. onClick={() => setEmailProvider('mailgun')}
  245. className="flex-1"
  246. >
  247. Mailgun
  248. </Button>
  249. <Button
  250. type="button"
  251. variant={emailProvider === 'sendgrid' ? 'default' : 'outline'}
  252. onClick={() => setEmailProvider('sendgrid')}
  253. className="flex-1"
  254. >
  255. SendGrid
  256. </Button>
  257. <Button
  258. type="button"
  259. variant={emailProvider === 'ses' ? 'default' : 'outline'}
  260. onClick={() => setEmailProvider('ses')}
  261. className="flex-1"
  262. >
  263. Amazon SES
  264. </Button>
  265. </div>
  266. {emailProvider === 'smtp' && (
  267. <>
  268. <div className="grid gap-4 sm:grid-cols-2">
  269. <div className="space-y-2">
  270. <Label htmlFor="smtp-host">SMTP Host</Label>
  271. <Input
  272. id="smtp-host"
  273. placeholder="smtp.example.com"
  274. value={smtpHost}
  275. onChange={(e) => setSmtpHost(e.target.value)}
  276. />
  277. </div>
  278. <div className="space-y-2">
  279. <Label htmlFor="smtp-port">SMTP Port</Label>
  280. <Input
  281. id="smtp-port"
  282. placeholder="587"
  283. value={smtpPort}
  284. onChange={(e) => setSmtpPort(e.target.value)}
  285. />
  286. </div>
  287. </div>
  288. <div className="grid gap-4 sm:grid-cols-2">
  289. <div className="space-y-2">
  290. <Label htmlFor="smtp-user">Username</Label>
  291. <Input
  292. id="smtp-user"
  293. placeholder="user@example.com"
  294. value={smtpUser}
  295. onChange={(e) => setSmtpUser(e.target.value)}
  296. />
  297. </div>
  298. <div className="space-y-2">
  299. <Label htmlFor="smtp-pass">Password</Label>
  300. <Input
  301. id="smtp-pass"
  302. type="password"
  303. placeholder="••••••••"
  304. value={smtpPass}
  305. onChange={(e) => setSmtpPass(e.target.value)}
  306. />
  307. </div>
  308. </div>
  309. <div className="grid gap-4 sm:grid-cols-2">
  310. <div className="space-y-2">
  311. <Label htmlFor="smtp-from-email">From Email</Label>
  312. <Input
  313. id="smtp-from-email"
  314. placeholder="noreply@example.com"
  315. value={smtpFromEmail}
  316. onChange={(e) => setSmtpFromEmail(e.target.value)}
  317. />
  318. </div>
  319. <div className="space-y-2">
  320. <Label htmlFor="smtp-from-name">From Name</Label>
  321. <Input
  322. id="smtp-from-name"
  323. placeholder="Torqvoice"
  324. value={smtpFromName}
  325. onChange={(e) => setSmtpFromName(e.target.value)}
  326. />
  327. </div>
  328. </div>
  329. <div className="space-y-4">
  330. <div className="flex items-center justify-between">
  331. <div className="space-y-0.5">
  332. <Label htmlFor="smtp-secure">TLS Connection (Port 465)</Label>
  333. <p className="text-xs text-muted-foreground">
  334. Enable for implicit TLS. Disable for STARTTLS (port 587/25).
  335. </p>
  336. </div>
  337. <Switch id="smtp-secure" checked={smtpSecure} onCheckedChange={setSmtpSecure} />
  338. </div>
  339. <div className="flex items-center justify-between">
  340. <div className="space-y-0.5">
  341. <Label htmlFor="smtp-reject-unauthorized">Verify TLS Certificates</Label>
  342. <p className="text-xs text-muted-foreground">
  343. Disable to allow self-signed certificates (not recommended for production)
  344. </p>
  345. </div>
  346. <Switch
  347. id="smtp-reject-unauthorized"
  348. checked={smtpRejectUnauthorized}
  349. onCheckedChange={setSmtpRejectUnauthorized}
  350. />
  351. </div>
  352. <div className="flex items-center justify-between">
  353. <div className="space-y-0.5">
  354. <Label htmlFor="smtp-require-tls">Require TLS Upgrade</Label>
  355. <p className="text-xs text-muted-foreground">
  356. Force TLS upgrade on STARTTLS connections
  357. </p>
  358. </div>
  359. <Switch
  360. id="smtp-require-tls"
  361. checked={smtpRequireTls}
  362. onCheckedChange={setSmtpRequireTls}
  363. />
  364. </div>
  365. </div>
  366. </>
  367. )}
  368. {emailProvider === 'resend' && (
  369. <>
  370. <div className="space-y-2">
  371. <Label htmlFor="resend-api-key">API Key</Label>
  372. <Input
  373. id="resend-api-key"
  374. type="password"
  375. placeholder="re_••••••••"
  376. value={resendApiKey}
  377. onChange={(e) => setResendApiKey(e.target.value)}
  378. />
  379. <p className="text-xs text-muted-foreground">
  380. Get your API key from{' '}
  381. <a
  382. href="https://resend.com"
  383. target="_blank"
  384. rel="noopener noreferrer"
  385. className="underline"
  386. >
  387. resend.com
  388. </a>
  389. </p>
  390. </div>
  391. <div className="grid gap-4 sm:grid-cols-2">
  392. <div className="space-y-2">
  393. <Label htmlFor="resend-from-email">From Email</Label>
  394. <Input
  395. id="resend-from-email"
  396. placeholder="noreply@yourdomain.com"
  397. value={resendFromEmail}
  398. onChange={(e) => setResendFromEmail(e.target.value)}
  399. />
  400. </div>
  401. <div className="space-y-2">
  402. <Label htmlFor="resend-from-name">From Name</Label>
  403. <Input
  404. id="resend-from-name"
  405. placeholder="Torqvoice"
  406. value={resendFromName}
  407. onChange={(e) => setResendFromName(e.target.value)}
  408. />
  409. </div>
  410. </div>
  411. </>
  412. )}
  413. {emailProvider === 'postmark' && (
  414. <>
  415. <div className="space-y-2">
  416. <Label htmlFor="postmark-api-key">Server Token</Label>
  417. <Input
  418. id="postmark-api-key"
  419. type="password"
  420. placeholder="••••••••-••••-••••-••••-••••••••••••"
  421. value={postmarkApiKey}
  422. onChange={(e) => setPostmarkApiKey(e.target.value)}
  423. />
  424. <p className="text-xs text-muted-foreground">
  425. Get your Server Token from{' '}
  426. <a
  427. href="https://postmarkapp.com"
  428. target="_blank"
  429. rel="noopener noreferrer"
  430. className="underline"
  431. >
  432. postmarkapp.com
  433. </a>
  434. </p>
  435. </div>
  436. <div className="grid gap-4 sm:grid-cols-2">
  437. <div className="space-y-2">
  438. <Label htmlFor="postmark-from-email">From Email</Label>
  439. <Input
  440. id="postmark-from-email"
  441. placeholder="noreply@yourdomain.com"
  442. value={postmarkFromEmail}
  443. onChange={(e) => setPostmarkFromEmail(e.target.value)}
  444. />
  445. </div>
  446. <div className="space-y-2">
  447. <Label htmlFor="postmark-from-name">From Name</Label>
  448. <Input
  449. id="postmark-from-name"
  450. placeholder="Torqvoice"
  451. value={postmarkFromName}
  452. onChange={(e) => setPostmarkFromName(e.target.value)}
  453. />
  454. </div>
  455. </div>
  456. </>
  457. )}
  458. {emailProvider === 'mailgun' && (
  459. <>
  460. <div className="space-y-2">
  461. <Label htmlFor="mailgun-api-key">API Key</Label>
  462. <Input
  463. id="mailgun-api-key"
  464. type="password"
  465. placeholder="key-••••••••••••••••••••••••••••••••"
  466. value={mailgunApiKey}
  467. onChange={(e) => setMailgunApiKey(e.target.value)}
  468. />
  469. <p className="text-xs text-muted-foreground">
  470. Get your API key from{' '}
  471. <a
  472. href="https://app.mailgun.com"
  473. target="_blank"
  474. rel="noopener noreferrer"
  475. className="underline"
  476. >
  477. app.mailgun.com
  478. </a>
  479. </p>
  480. </div>
  481. <div className="grid gap-4 sm:grid-cols-2">
  482. <div className="space-y-2">
  483. <Label htmlFor="mailgun-domain">Domain</Label>
  484. <Input
  485. id="mailgun-domain"
  486. placeholder="mg.yourdomain.com"
  487. value={mailgunDomain}
  488. onChange={(e) => setMailgunDomain(e.target.value)}
  489. />
  490. </div>
  491. <div className="space-y-2">
  492. <Label htmlFor="mailgun-region">Region</Label>
  493. <div className="flex gap-2">
  494. <Button
  495. type="button"
  496. variant={mailgunRegion === 'us' ? 'default' : 'outline'}
  497. onClick={() => setMailgunRegion('us')}
  498. className="flex-1"
  499. size="sm"
  500. >
  501. US
  502. </Button>
  503. <Button
  504. type="button"
  505. variant={mailgunRegion === 'eu' ? 'default' : 'outline'}
  506. onClick={() => setMailgunRegion('eu')}
  507. className="flex-1"
  508. size="sm"
  509. >
  510. EU
  511. </Button>
  512. </div>
  513. </div>
  514. </div>
  515. <div className="grid gap-4 sm:grid-cols-2">
  516. <div className="space-y-2">
  517. <Label htmlFor="mailgun-from-email">From Email</Label>
  518. <Input
  519. id="mailgun-from-email"
  520. placeholder="noreply@yourdomain.com"
  521. value={mailgunFromEmail}
  522. onChange={(e) => setMailgunFromEmail(e.target.value)}
  523. />
  524. </div>
  525. <div className="space-y-2">
  526. <Label htmlFor="mailgun-from-name">From Name</Label>
  527. <Input
  528. id="mailgun-from-name"
  529. placeholder="Torqvoice"
  530. value={mailgunFromName}
  531. onChange={(e) => setMailgunFromName(e.target.value)}
  532. />
  533. </div>
  534. </div>
  535. </>
  536. )}
  537. {emailProvider === 'sendgrid' && (
  538. <>
  539. <div className="space-y-2">
  540. <Label htmlFor="sendgrid-api-key">API Key</Label>
  541. <Input
  542. id="sendgrid-api-key"
  543. type="password"
  544. placeholder="SG.••••••••••••••••••••••••••••••••"
  545. value={sendgridApiKey}
  546. onChange={(e) => setSendgridApiKey(e.target.value)}
  547. />
  548. <p className="text-xs text-muted-foreground">
  549. Get your API key from{' '}
  550. <a
  551. href="https://app.sendgrid.com"
  552. target="_blank"
  553. rel="noopener noreferrer"
  554. className="underline"
  555. >
  556. app.sendgrid.com
  557. </a>
  558. </p>
  559. </div>
  560. <div className="grid gap-4 sm:grid-cols-2">
  561. <div className="space-y-2">
  562. <Label htmlFor="sendgrid-from-email">From Email</Label>
  563. <Input
  564. id="sendgrid-from-email"
  565. placeholder="noreply@yourdomain.com"
  566. value={sendgridFromEmail}
  567. onChange={(e) => setSendgridFromEmail(e.target.value)}
  568. />
  569. </div>
  570. <div className="space-y-2">
  571. <Label htmlFor="sendgrid-from-name">From Name</Label>
  572. <Input
  573. id="sendgrid-from-name"
  574. placeholder="Torqvoice"
  575. value={sendgridFromName}
  576. onChange={(e) => setSendgridFromName(e.target.value)}
  577. />
  578. </div>
  579. </div>
  580. </>
  581. )}
  582. {emailProvider === 'ses' && (
  583. <>
  584. <div className="grid gap-4 sm:grid-cols-2">
  585. <div className="space-y-2">
  586. <Label htmlFor="ses-access-key">Access Key ID</Label>
  587. <Input
  588. id="ses-access-key"
  589. type="password"
  590. placeholder="AKIA••••••••••••••••"
  591. value={sesAccessKeyId}
  592. onChange={(e) => setSesAccessKeyId(e.target.value)}
  593. />
  594. </div>
  595. <div className="space-y-2">
  596. <Label htmlFor="ses-secret-key">Secret Access Key</Label>
  597. <Input
  598. id="ses-secret-key"
  599. type="password"
  600. placeholder="••••••••••••••••••••••••••••••••••••••••"
  601. value={sesSecretAccessKey}
  602. onChange={(e) => setSesSecretAccessKey(e.target.value)}
  603. />
  604. </div>
  605. </div>
  606. <div className="space-y-2">
  607. <Label htmlFor="ses-region">AWS Region</Label>
  608. <Input
  609. id="ses-region"
  610. placeholder="us-east-1"
  611. value={sesRegion}
  612. onChange={(e) => setSesRegion(e.target.value)}
  613. />
  614. <p className="text-xs text-muted-foreground">
  615. The AWS region where SES is configured (e.g. us-east-1, eu-west-1)
  616. </p>
  617. </div>
  618. <div className="grid gap-4 sm:grid-cols-2">
  619. <div className="space-y-2">
  620. <Label htmlFor="ses-from-email">From Email</Label>
  621. <Input
  622. id="ses-from-email"
  623. placeholder="noreply@yourdomain.com"
  624. value={sesFromEmail}
  625. onChange={(e) => setSesFromEmail(e.target.value)}
  626. />
  627. </div>
  628. <div className="space-y-2">
  629. <Label htmlFor="ses-from-name">From Name</Label>
  630. <Input
  631. id="ses-from-name"
  632. placeholder="Torqvoice"
  633. value={sesFromName}
  634. onChange={(e) => setSesFromName(e.target.value)}
  635. />
  636. </div>
  637. </div>
  638. </>
  639. )}
  640. <div className="flex items-center gap-2 pt-2">
  641. <Button
  642. type="button"
  643. variant="outline"
  644. onClick={handleTestEmail}
  645. disabled={isTestDisabled}
  646. >
  647. {isTesting ? (
  648. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  649. ) : (
  650. <Send className="mr-2 h-4 w-4" />
  651. )}
  652. Send Test Email
  653. </Button>
  654. <p className="text-xs text-muted-foreground">
  655. Save settings first, then send a test email to your account
  656. </p>
  657. </div>
  658. </CardContent>
  659. </Card>
  660. {/* Save Button */}
  661. <div className="flex justify-end">
  662. <Button onClick={handleSave} disabled={isPending}>
  663. {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
  664. Save Settings
  665. </Button>
  666. </div>
  667. </div>
  668. )
  669. }