|
|
@@ -14,6 +14,8 @@ import type { SystemSettingsMap } from '../Schema/systemSettingsSchema'
|
|
|
import { setSystemSettings } from '../Actions/setSystemSettings'
|
|
|
import { testEmailConnection } from '../Actions/testEmailConnection'
|
|
|
|
|
|
+type EmailProviderType = 'smtp' | 'resend' | 'postmark' | 'mailgun' | 'sendgrid' | 'ses'
|
|
|
+
|
|
|
export function AdminSettings({
|
|
|
initial,
|
|
|
}: {
|
|
|
@@ -30,8 +32,8 @@ export function AdminSettings({
|
|
|
)
|
|
|
|
|
|
// Email provider
|
|
|
- const [emailProvider, setEmailProvider] = useState<'smtp' | 'resend'>(
|
|
|
- (initial[SYSTEM_SETTING_KEYS.EMAIL_PROVIDER] as 'smtp' | 'resend') || 'smtp'
|
|
|
+ const [emailProvider, setEmailProvider] = useState<EmailProviderType>(
|
|
|
+ (initial[SYSTEM_SETTING_KEYS.EMAIL_PROVIDER] as EmailProviderType) || 'smtp'
|
|
|
)
|
|
|
|
|
|
// SMTP settings
|
|
|
@@ -64,11 +66,68 @@ export function AdminSettings({
|
|
|
initial[SYSTEM_SETTING_KEYS.RESEND_FROM_NAME] || ''
|
|
|
)
|
|
|
|
|
|
+ // Postmark settings
|
|
|
+ const [postmarkApiKey, setPostmarkApiKey] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.POSTMARK_API_KEY] || ''
|
|
|
+ )
|
|
|
+ const [postmarkFromEmail, setPostmarkFromEmail] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL] || ''
|
|
|
+ )
|
|
|
+ const [postmarkFromName, setPostmarkFromName] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME] || ''
|
|
|
+ )
|
|
|
+
|
|
|
+ // Mailgun settings
|
|
|
+ const [mailgunApiKey, setMailgunApiKey] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.MAILGUN_API_KEY] || ''
|
|
|
+ )
|
|
|
+ const [mailgunDomain, setMailgunDomain] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN] || ''
|
|
|
+ )
|
|
|
+ const [mailgunRegion, setMailgunRegion] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.MAILGUN_REGION] || 'us'
|
|
|
+ )
|
|
|
+ const [mailgunFromEmail, setMailgunFromEmail] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL] || ''
|
|
|
+ )
|
|
|
+ const [mailgunFromName, setMailgunFromName] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME] || ''
|
|
|
+ )
|
|
|
+
|
|
|
+ // SendGrid settings
|
|
|
+ const [sendgridApiKey, setSendgridApiKey] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SENDGRID_API_KEY] || ''
|
|
|
+ )
|
|
|
+ const [sendgridFromEmail, setSendgridFromEmail] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL] || ''
|
|
|
+ )
|
|
|
+ const [sendgridFromName, setSendgridFromName] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME] || ''
|
|
|
+ )
|
|
|
+
|
|
|
+ // Amazon SES settings
|
|
|
+ const [sesAccessKeyId, setSesAccessKeyId] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID] || ''
|
|
|
+ )
|
|
|
+ const [sesSecretAccessKey, setSesSecretAccessKey] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY] || ''
|
|
|
+ )
|
|
|
+ const [sesRegion, setSesRegion] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SES_REGION] || 'us-east-1'
|
|
|
+ )
|
|
|
+ const [sesFromEmail, setSesFromEmail] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SES_FROM_EMAIL] || ''
|
|
|
+ )
|
|
|
+ const [sesFromName, setSesFromName] = useState(
|
|
|
+ initial[SYSTEM_SETTING_KEYS.SES_FROM_NAME] || ''
|
|
|
+ )
|
|
|
+
|
|
|
const handleSave = () => {
|
|
|
startTransition(async () => {
|
|
|
const data: Record<string, string> = {
|
|
|
[SYSTEM_SETTING_KEYS.REGISTRATION_DISABLED]: String(registrationDisabled),
|
|
|
[SYSTEM_SETTING_KEYS.EMAIL_PROVIDER]: emailProvider,
|
|
|
+ // SMTP
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_HOST]: smtpHost,
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_PORT]: smtpPort,
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_USER]: smtpUser,
|
|
|
@@ -78,9 +137,30 @@ export function AdminSettings({
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_FROM_NAME]: smtpFromName,
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED]: String(smtpRejectUnauthorized),
|
|
|
[SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS]: String(smtpRequireTls),
|
|
|
+ // Resend
|
|
|
[SYSTEM_SETTING_KEYS.RESEND_API_KEY]: resendApiKey,
|
|
|
[SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL]: resendFromEmail,
|
|
|
[SYSTEM_SETTING_KEYS.RESEND_FROM_NAME]: resendFromName,
|
|
|
+ // Postmark
|
|
|
+ [SYSTEM_SETTING_KEYS.POSTMARK_API_KEY]: postmarkApiKey,
|
|
|
+ [SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL]: postmarkFromEmail,
|
|
|
+ [SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME]: postmarkFromName,
|
|
|
+ // Mailgun
|
|
|
+ [SYSTEM_SETTING_KEYS.MAILGUN_API_KEY]: mailgunApiKey,
|
|
|
+ [SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN]: mailgunDomain,
|
|
|
+ [SYSTEM_SETTING_KEYS.MAILGUN_REGION]: mailgunRegion,
|
|
|
+ [SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL]: mailgunFromEmail,
|
|
|
+ [SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME]: mailgunFromName,
|
|
|
+ // SendGrid
|
|
|
+ [SYSTEM_SETTING_KEYS.SENDGRID_API_KEY]: sendgridApiKey,
|
|
|
+ [SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL]: sendgridFromEmail,
|
|
|
+ [SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME]: sendgridFromName,
|
|
|
+ // Amazon SES
|
|
|
+ [SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID]: sesAccessKeyId,
|
|
|
+ [SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY]: sesSecretAccessKey,
|
|
|
+ [SYSTEM_SETTING_KEYS.SES_REGION]: sesRegion,
|
|
|
+ [SYSTEM_SETTING_KEYS.SES_FROM_EMAIL]: sesFromEmail,
|
|
|
+ [SYSTEM_SETTING_KEYS.SES_FROM_NAME]: sesFromName,
|
|
|
}
|
|
|
|
|
|
const result = await setSystemSettings(data)
|
|
|
@@ -110,7 +190,11 @@ export function AdminSettings({
|
|
|
const isTestDisabled =
|
|
|
isTesting ||
|
|
|
(emailProvider === 'smtp' && !smtpHost) ||
|
|
|
- (emailProvider === 'resend' && !resendApiKey)
|
|
|
+ (emailProvider === 'resend' && !resendApiKey) ||
|
|
|
+ (emailProvider === 'postmark' && !postmarkApiKey) ||
|
|
|
+ (emailProvider === 'mailgun' && !mailgunApiKey) ||
|
|
|
+ (emailProvider === 'sendgrid' && !sendgridApiKey) ||
|
|
|
+ (emailProvider === 'ses' && !sesAccessKeyId)
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-6">
|
|
|
@@ -146,7 +230,7 @@ export function AdminSettings({
|
|
|
</CardDescription>
|
|
|
</CardHeader>
|
|
|
<CardContent className="space-y-6">
|
|
|
- <div className="flex gap-2">
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
<Button
|
|
|
type="button"
|
|
|
variant={emailProvider === 'smtp' ? 'default' : 'outline'}
|
|
|
@@ -163,6 +247,38 @@ export function AdminSettings({
|
|
|
>
|
|
|
Resend
|
|
|
</Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={emailProvider === 'postmark' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setEmailProvider('postmark')}
|
|
|
+ className="flex-1"
|
|
|
+ >
|
|
|
+ Postmark
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={emailProvider === 'mailgun' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setEmailProvider('mailgun')}
|
|
|
+ className="flex-1"
|
|
|
+ >
|
|
|
+ Mailgun
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={emailProvider === 'sendgrid' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setEmailProvider('sendgrid')}
|
|
|
+ className="flex-1"
|
|
|
+ >
|
|
|
+ SendGrid
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={emailProvider === 'ses' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setEmailProvider('ses')}
|
|
|
+ className="flex-1"
|
|
|
+ >
|
|
|
+ Amazon SES
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
|
|
|
{emailProvider === 'smtp' && (
|
|
|
@@ -320,6 +436,243 @@ export function AdminSettings({
|
|
|
</>
|
|
|
)}
|
|
|
|
|
|
+ {emailProvider === 'postmark' && (
|
|
|
+ <>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="postmark-api-key">Server Token</Label>
|
|
|
+ <Input
|
|
|
+ id="postmark-api-key"
|
|
|
+ type="password"
|
|
|
+ placeholder="••••••••-••••-••••-••••-••••••••••••"
|
|
|
+ value={postmarkApiKey}
|
|
|
+ onChange={(e) => setPostmarkApiKey(e.target.value)}
|
|
|
+ />
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ Get your Server Token from{' '}
|
|
|
+ <a
|
|
|
+ href="https://postmarkapp.com"
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="underline"
|
|
|
+ >
|
|
|
+ postmarkapp.com
|
|
|
+ </a>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="postmark-from-email">From Email</Label>
|
|
|
+ <Input
|
|
|
+ id="postmark-from-email"
|
|
|
+ placeholder="noreply@yourdomain.com"
|
|
|
+ value={postmarkFromEmail}
|
|
|
+ onChange={(e) => setPostmarkFromEmail(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="postmark-from-name">From Name</Label>
|
|
|
+ <Input
|
|
|
+ id="postmark-from-name"
|
|
|
+ placeholder="Torqvoice"
|
|
|
+ value={postmarkFromName}
|
|
|
+ onChange={(e) => setPostmarkFromName(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {emailProvider === 'mailgun' && (
|
|
|
+ <>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="mailgun-api-key">API Key</Label>
|
|
|
+ <Input
|
|
|
+ id="mailgun-api-key"
|
|
|
+ type="password"
|
|
|
+ placeholder="key-••••••••••••••••••••••••••••••••"
|
|
|
+ value={mailgunApiKey}
|
|
|
+ onChange={(e) => setMailgunApiKey(e.target.value)}
|
|
|
+ />
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ Get your API key from{' '}
|
|
|
+ <a
|
|
|
+ href="https://app.mailgun.com"
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="underline"
|
|
|
+ >
|
|
|
+ app.mailgun.com
|
|
|
+ </a>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="mailgun-domain">Domain</Label>
|
|
|
+ <Input
|
|
|
+ id="mailgun-domain"
|
|
|
+ placeholder="mg.yourdomain.com"
|
|
|
+ value={mailgunDomain}
|
|
|
+ onChange={(e) => setMailgunDomain(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="mailgun-region">Region</Label>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={mailgunRegion === 'us' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setMailgunRegion('us')}
|
|
|
+ className="flex-1"
|
|
|
+ size="sm"
|
|
|
+ >
|
|
|
+ US
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant={mailgunRegion === 'eu' ? 'default' : 'outline'}
|
|
|
+ onClick={() => setMailgunRegion('eu')}
|
|
|
+ className="flex-1"
|
|
|
+ size="sm"
|
|
|
+ >
|
|
|
+ EU
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="mailgun-from-email">From Email</Label>
|
|
|
+ <Input
|
|
|
+ id="mailgun-from-email"
|
|
|
+ placeholder="noreply@yourdomain.com"
|
|
|
+ value={mailgunFromEmail}
|
|
|
+ onChange={(e) => setMailgunFromEmail(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="mailgun-from-name">From Name</Label>
|
|
|
+ <Input
|
|
|
+ id="mailgun-from-name"
|
|
|
+ placeholder="Torqvoice"
|
|
|
+ value={mailgunFromName}
|
|
|
+ onChange={(e) => setMailgunFromName(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {emailProvider === 'sendgrid' && (
|
|
|
+ <>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="sendgrid-api-key">API Key</Label>
|
|
|
+ <Input
|
|
|
+ id="sendgrid-api-key"
|
|
|
+ type="password"
|
|
|
+ placeholder="SG.••••••••••••••••••••••••••••••••"
|
|
|
+ value={sendgridApiKey}
|
|
|
+ onChange={(e) => setSendgridApiKey(e.target.value)}
|
|
|
+ />
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ Get your API key from{' '}
|
|
|
+ <a
|
|
|
+ href="https://app.sendgrid.com"
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="underline"
|
|
|
+ >
|
|
|
+ app.sendgrid.com
|
|
|
+ </a>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="sendgrid-from-email">From Email</Label>
|
|
|
+ <Input
|
|
|
+ id="sendgrid-from-email"
|
|
|
+ placeholder="noreply@yourdomain.com"
|
|
|
+ value={sendgridFromEmail}
|
|
|
+ onChange={(e) => setSendgridFromEmail(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="sendgrid-from-name">From Name</Label>
|
|
|
+ <Input
|
|
|
+ id="sendgrid-from-name"
|
|
|
+ placeholder="Torqvoice"
|
|
|
+ value={sendgridFromName}
|
|
|
+ onChange={(e) => setSendgridFromName(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {emailProvider === 'ses' && (
|
|
|
+ <>
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ses-access-key">Access Key ID</Label>
|
|
|
+ <Input
|
|
|
+ id="ses-access-key"
|
|
|
+ type="password"
|
|
|
+ placeholder="AKIA••••••••••••••••"
|
|
|
+ value={sesAccessKeyId}
|
|
|
+ onChange={(e) => setSesAccessKeyId(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ses-secret-key">Secret Access Key</Label>
|
|
|
+ <Input
|
|
|
+ id="ses-secret-key"
|
|
|
+ type="password"
|
|
|
+ placeholder="••••••••••••••••••••••••••••••••••••••••"
|
|
|
+ value={sesSecretAccessKey}
|
|
|
+ onChange={(e) => setSesSecretAccessKey(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ses-region">AWS Region</Label>
|
|
|
+ <Input
|
|
|
+ id="ses-region"
|
|
|
+ placeholder="us-east-1"
|
|
|
+ value={sesRegion}
|
|
|
+ onChange={(e) => setSesRegion(e.target.value)}
|
|
|
+ />
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ The AWS region where SES is configured (e.g. us-east-1, eu-west-1)
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ses-from-email">From Email</Label>
|
|
|
+ <Input
|
|
|
+ id="ses-from-email"
|
|
|
+ placeholder="noreply@yourdomain.com"
|
|
|
+ value={sesFromEmail}
|
|
|
+ onChange={(e) => setSesFromEmail(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ses-from-name">From Name</Label>
|
|
|
+ <Input
|
|
|
+ id="ses-from-name"
|
|
|
+ placeholder="Torqvoice"
|
|
|
+ value={sesFromName}
|
|
|
+ onChange={(e) => setSesFromName(e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
<div className="flex items-center gap-2 pt-2">
|
|
|
<Button
|
|
|
type="button"
|