Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
03215b5ead

Разница между файлами не показана из-за своего большого размера
+ 1390 - 100
package-lock.json


+ 5 - 0
package.json

@@ -13,9 +13,11 @@
     "check:fix": "biome check . --write"
     "check:fix": "biome check . --write"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@aws-sdk/client-ses": "^3.990.0",
     "@hookform/resolvers": "^5.2.2",
     "@hookform/resolvers": "^5.2.2",
     "@prisma/client": "^6.19.2",
     "@prisma/client": "^6.19.2",
     "@react-pdf/renderer": "^4.3.2",
     "@react-pdf/renderer": "^4.3.2",
+    "@sendgrid/mail": "^8.1.6",
     "@tanstack/react-query": "^5.90.21",
     "@tanstack/react-query": "^5.90.21",
     "archiver": "^7.0.1",
     "archiver": "^7.0.1",
     "better-auth": "^1.4.18",
     "better-auth": "^1.4.18",
@@ -24,12 +26,15 @@
     "clsx": "^2.1.1",
     "clsx": "^2.1.1",
     "cmdk": "^1.1.1",
     "cmdk": "^1.1.1",
     "date-fns": "^4.1.0",
     "date-fns": "^4.1.0",
+    "form-data": "^4.0.5",
     "jszip": "^3.10.1",
     "jszip": "^3.10.1",
     "lucide-react": "^0.563.0",
     "lucide-react": "^0.563.0",
+    "mailgun.js": "^12.7.0",
     "next": "16.1.6",
     "next": "16.1.6",
     "next-themes": "^0.4.6",
     "next-themes": "^0.4.6",
     "nodemailer": "^8.0.1",
     "nodemailer": "^8.0.1",
     "pdf-lib": "^1.17.1",
     "pdf-lib": "^1.17.1",
+    "postmark": "^4.0.5",
     "prisma": "^6.19.2",
     "prisma": "^6.19.2",
     "qrcode.react": "^4.2.0",
     "qrcode.react": "^4.2.0",
     "radix-ui": "^1.4.3",
     "radix-ui": "^1.4.3",

+ 5 - 4
plans/project_overview.md

@@ -18,7 +18,8 @@ For submenus, use same layout as /settings were we have a child sidebar.
 
 
 Task List:
 Task List:
 
 
-- First user that register on application should automatically be super admin.
-- In the new super admin page, add new sidebar authentication and add option to disable new user registrations. also add other options that
-  you think is usefull for super admin for this type of applications.
-- Add email settings for super admin to set app wider email smtp setup with test mail function. this has to include all settings for smtp, tls starttls and so on. look online so we get all settings correct. 
+- Create email providers:
+  - Postmark
+  - Mailgun
+  - SendGrid
+  - Amazon SES

+ 3 - 28
src/features/admin/Actions/testEmailConnection.ts

@@ -2,7 +2,7 @@
 
 
 import { withSuperAdmin } from "@/lib/with-super-admin";
 import { withSuperAdmin } from "@/lib/with-super-admin";
 import { db } from "@/lib/db";
 import { db } from "@/lib/db";
-import { sendMail } from "@/lib/email";
+import { sendMail, getFromAddress } from "@/lib/email";
 import { SYSTEM_SETTING_KEYS } from "../Schema/systemSettingsSchema";
 import { SYSTEM_SETTING_KEYS } from "../Schema/systemSettingsSchema";
 
 
 export async function testEmailConnection() {
 export async function testEmailConnection() {
@@ -16,36 +16,11 @@ export async function testEmailConnection() {
       throw new Error("Could not find your email address");
       throw new Error("Could not find your email address");
     }
     }
 
 
-    // Determine provider
     const providerSetting = await db.systemSetting.findUnique({
     const providerSetting = await db.systemSetting.findUnique({
       where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
       where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
     });
     });
-    const provider = providerSetting?.value === "resend" ? "resend" : "smtp";
-
-    // Get from address based on provider
-    let from: string;
-    if (provider === "resend") {
-      const fromSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL },
-      });
-      const fromName = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_NAME },
-      });
-      const fromEmail = fromSetting?.value || user.email;
-      const senderName = fromName?.value || "Torqvoice";
-      from = `${senderName} <${fromEmail}>`;
-    } else {
-      const fromSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL },
-      });
-      const fromName = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_NAME },
-      });
-      const fromEmail =
-        fromSetting?.value || process.env.SMTP_FROM_EMAIL || user.email;
-      const senderName = fromName?.value || "Torqvoice";
-      from = `${senderName} <${fromEmail}>`;
-    }
+    const provider = providerSetting?.value || "smtp";
+    const from = await getFromAddress();
 
 
     await sendMail({
     await sendMail({
       from,
       from,

+ 357 - 4
src/features/admin/Components/admin-settings.tsx

@@ -14,6 +14,8 @@ import type { SystemSettingsMap } from '../Schema/systemSettingsSchema'
 import { setSystemSettings } from '../Actions/setSystemSettings'
 import { setSystemSettings } from '../Actions/setSystemSettings'
 import { testEmailConnection } from '../Actions/testEmailConnection'
 import { testEmailConnection } from '../Actions/testEmailConnection'
 
 
+type EmailProviderType = 'smtp' | 'resend' | 'postmark' | 'mailgun' | 'sendgrid' | 'ses'
+
 export function AdminSettings({
 export function AdminSettings({
   initial,
   initial,
 }: {
 }: {
@@ -30,8 +32,8 @@ export function AdminSettings({
   )
   )
 
 
   // Email provider
   // 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
   // SMTP settings
@@ -64,11 +66,68 @@ export function AdminSettings({
     initial[SYSTEM_SETTING_KEYS.RESEND_FROM_NAME] || ''
     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 = () => {
   const handleSave = () => {
     startTransition(async () => {
     startTransition(async () => {
       const data: Record<string, string> = {
       const data: Record<string, string> = {
         [SYSTEM_SETTING_KEYS.REGISTRATION_DISABLED]: String(registrationDisabled),
         [SYSTEM_SETTING_KEYS.REGISTRATION_DISABLED]: String(registrationDisabled),
         [SYSTEM_SETTING_KEYS.EMAIL_PROVIDER]: emailProvider,
         [SYSTEM_SETTING_KEYS.EMAIL_PROVIDER]: emailProvider,
+        // SMTP
         [SYSTEM_SETTING_KEYS.SMTP_HOST]: smtpHost,
         [SYSTEM_SETTING_KEYS.SMTP_HOST]: smtpHost,
         [SYSTEM_SETTING_KEYS.SMTP_PORT]: smtpPort,
         [SYSTEM_SETTING_KEYS.SMTP_PORT]: smtpPort,
         [SYSTEM_SETTING_KEYS.SMTP_USER]: smtpUser,
         [SYSTEM_SETTING_KEYS.SMTP_USER]: smtpUser,
@@ -78,9 +137,30 @@ export function AdminSettings({
         [SYSTEM_SETTING_KEYS.SMTP_FROM_NAME]: smtpFromName,
         [SYSTEM_SETTING_KEYS.SMTP_FROM_NAME]: smtpFromName,
         [SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED]: String(smtpRejectUnauthorized),
         [SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED]: String(smtpRejectUnauthorized),
         [SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS]: String(smtpRequireTls),
         [SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS]: String(smtpRequireTls),
+        // Resend
         [SYSTEM_SETTING_KEYS.RESEND_API_KEY]: resendApiKey,
         [SYSTEM_SETTING_KEYS.RESEND_API_KEY]: resendApiKey,
         [SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL]: resendFromEmail,
         [SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL]: resendFromEmail,
         [SYSTEM_SETTING_KEYS.RESEND_FROM_NAME]: resendFromName,
         [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)
       const result = await setSystemSettings(data)
@@ -110,7 +190,11 @@ export function AdminSettings({
   const isTestDisabled =
   const isTestDisabled =
     isTesting ||
     isTesting ||
     (emailProvider === 'smtp' && !smtpHost) ||
     (emailProvider === 'smtp' && !smtpHost) ||
-    (emailProvider === 'resend' && !resendApiKey)
+    (emailProvider === 'resend' && !resendApiKey) ||
+    (emailProvider === 'postmark' && !postmarkApiKey) ||
+    (emailProvider === 'mailgun' && !mailgunApiKey) ||
+    (emailProvider === 'sendgrid' && !sendgridApiKey) ||
+    (emailProvider === 'ses' && !sesAccessKeyId)
 
 
   return (
   return (
     <div className="space-y-6">
     <div className="space-y-6">
@@ -146,7 +230,7 @@ export function AdminSettings({
           </CardDescription>
           </CardDescription>
         </CardHeader>
         </CardHeader>
         <CardContent className="space-y-6">
         <CardContent className="space-y-6">
-          <div className="flex gap-2">
+          <div className="flex flex-wrap gap-2">
             <Button
             <Button
               type="button"
               type="button"
               variant={emailProvider === 'smtp' ? 'default' : 'outline'}
               variant={emailProvider === 'smtp' ? 'default' : 'outline'}
@@ -163,6 +247,38 @@ export function AdminSettings({
             >
             >
               Resend
               Resend
             </Button>
             </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>
           </div>
 
 
           {emailProvider === 'smtp' && (
           {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">
           <div className="flex items-center gap-2 pt-2">
             <Button
             <Button
               type="button"
               type="button"

+ 24 - 0
src/features/admin/Schema/systemSettingsSchema.ts

@@ -22,6 +22,30 @@ export const SYSTEM_SETTING_KEYS = {
   RESEND_API_KEY: "resend.apiKey",
   RESEND_API_KEY: "resend.apiKey",
   RESEND_FROM_EMAIL: "resend.fromEmail",
   RESEND_FROM_EMAIL: "resend.fromEmail",
   RESEND_FROM_NAME: "resend.fromName",
   RESEND_FROM_NAME: "resend.fromName",
+
+  // Postmark
+  POSTMARK_API_KEY: "postmark.apiKey",
+  POSTMARK_FROM_EMAIL: "postmark.fromEmail",
+  POSTMARK_FROM_NAME: "postmark.fromName",
+
+  // Mailgun
+  MAILGUN_API_KEY: "mailgun.apiKey",
+  MAILGUN_DOMAIN: "mailgun.domain",
+  MAILGUN_REGION: "mailgun.region",
+  MAILGUN_FROM_EMAIL: "mailgun.fromEmail",
+  MAILGUN_FROM_NAME: "mailgun.fromName",
+
+  // SendGrid
+  SENDGRID_API_KEY: "sendgrid.apiKey",
+  SENDGRID_FROM_EMAIL: "sendgrid.fromEmail",
+  SENDGRID_FROM_NAME: "sendgrid.fromName",
+
+  // Amazon SES
+  SES_ACCESS_KEY_ID: "ses.accessKeyId",
+  SES_SECRET_ACCESS_KEY: "ses.secretAccessKey",
+  SES_REGION: "ses.region",
+  SES_FROM_EMAIL: "ses.fromEmail",
+  SES_FROM_NAME: "ses.fromName",
 } as const;
 } as const;
 
 
 export type SystemSettingKey =
 export type SystemSettingKey =

+ 3 - 34
src/features/team/Actions/sendInvitation.ts

@@ -4,8 +4,7 @@ import { randomUUID } from "crypto";
 import { db } from "@/lib/db";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { withAuth } from "@/lib/with-auth";
 import { sendInvitationSchema } from "../Schema/teamSchema";
 import { sendInvitationSchema } from "../Schema/teamSchema";
-import { sendMail } from "@/lib/email";
-import { SYSTEM_SETTING_KEYS } from "@/features/admin/Schema/systemSettingsSchema";
+import { sendMail, getFromAddress } from "@/lib/email";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { revalidatePath } from "next/cache";
 import { revalidatePath } from "next/cache";
 
 
@@ -46,43 +45,13 @@ export async function sendInvitation(input: unknown) {
       },
       },
     });
     });
 
 
-    // Resolve from address
-    const providerSetting = await db.systemSetting.findUnique({
-      where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
-    });
-    const provider = providerSetting?.value === "resend" ? "resend" : "smtp";
-
-    let fromEmail: string;
-    let fromName: string;
-
-    if (provider === "resend") {
-      const emailSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL },
-      });
-      const nameSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_NAME },
-      });
-      fromEmail = emailSetting?.value || "noreply@example.com";
-      fromName = nameSetting?.value || "Torqvoice";
-    } else {
-      const emailSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL },
-      });
-      const nameSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_NAME },
-      });
-      fromEmail =
-        emailSetting?.value ||
-        process.env.SMTP_FROM_EMAIL ||
-        "noreply@example.com";
-      fromName = nameSetting?.value || "Torqvoice";
-    }
+    const from = await getFromAddress();
 
 
     const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
     const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
     const signupUrl = `${baseUrl}/auth/sign-up?invite=${token}`;
     const signupUrl = `${baseUrl}/auth/sign-up?invite=${token}`;
 
 
     await sendMail({
     await sendMail({
-      from: `${fromName} <${fromEmail}>`,
+      from,
       to: data.email,
       to: data.email,
       subject: `You've been invited to join ${membership.organization.name} on Torqvoice`,
       subject: `You've been invited to join ${membership.organization.name} on Torqvoice`,
       html: `
       html: `

+ 3 - 38
src/lib/auth.ts

@@ -16,46 +16,11 @@ export const auth = betterAuth({
   emailAndPassword: {
   emailAndPassword: {
     enabled: true,
     enabled: true,
     sendResetPassword: async ({ user, url }) => {
     sendResetPassword: async ({ user, url }) => {
-      const { SYSTEM_SETTING_KEYS } = await import(
-        "@/features/admin/Schema/systemSettingsSchema"
-      );
+      const { sendMail, getFromAddress } = await import("@/lib/email");
+      const from = await getFromAddress();
 
 
-      // Determine provider and get from address
-      const providerSetting = await db.systemSetting.findUnique({
-        where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
-      });
-      const provider =
-        providerSetting?.value === "resend" ? "resend" : "smtp";
-
-      let fromEmail: string;
-      let fromName: string;
-
-      if (provider === "resend") {
-        const emailSetting = await db.systemSetting.findUnique({
-          where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL },
-        });
-        const nameSetting = await db.systemSetting.findUnique({
-          where: { key: SYSTEM_SETTING_KEYS.RESEND_FROM_NAME },
-        });
-        fromEmail = emailSetting?.value || "noreply@example.com";
-        fromName = nameSetting?.value || "Torqvoice";
-      } else {
-        const emailSetting = await db.systemSetting.findUnique({
-          where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL },
-        });
-        const nameSetting = await db.systemSetting.findUnique({
-          where: { key: SYSTEM_SETTING_KEYS.SMTP_FROM_NAME },
-        });
-        fromEmail =
-          emailSetting?.value ||
-          process.env.SMTP_FROM_EMAIL ||
-          "noreply@example.com";
-        fromName = nameSetting?.value || "Torqvoice";
-      }
-
-      const { sendMail } = await import("@/lib/email");
       await sendMail({
       await sendMail({
-        from: `${fromName} <${fromEmail}>`,
+        from,
         to: user.email,
         to: user.email,
         subject: "Reset your Torqvoice password",
         subject: "Reset your Torqvoice password",
         html: `
         html: `

+ 281 - 7
src/lib/email.ts

@@ -1,8 +1,21 @@
 import nodemailer from "nodemailer";
 import nodemailer from "nodemailer";
 import { Resend } from "resend";
 import { Resend } from "resend";
+import { ServerClient as PostmarkClient } from "postmark";
+import Mailgun from "mailgun.js";
+import FormData from "form-data";
+import sgMail, { type MailDataRequired } from "@sendgrid/mail";
+import { SESClient, SendRawEmailCommand } from "@aws-sdk/client-ses";
 import { db } from "./db";
 import { db } from "./db";
 import { SYSTEM_SETTING_KEYS } from "@/features/admin/Schema/systemSettingsSchema";
 import { SYSTEM_SETTING_KEYS } from "@/features/admin/Schema/systemSettingsSchema";
 
 
+export type EmailProvider =
+  | "smtp"
+  | "resend"
+  | "postmark"
+  | "mailgun"
+  | "sendgrid"
+  | "ses";
+
 export interface SendMailOptions {
 export interface SendMailOptions {
   from: string;
   from: string;
   to: string;
   to: string;
@@ -14,13 +27,71 @@ export interface SendMailOptions {
   }[];
   }[];
 }
 }
 
 
-async function getEmailProvider(): Promise<"smtp" | "resend"> {
+async function getEmailProvider(): Promise<EmailProvider> {
   const setting = await db.systemSetting.findUnique({
   const setting = await db.systemSetting.findUnique({
     where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
     where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
   });
   });
-  return setting?.value === "resend" ? "resend" : "smtp";
+  const value = setting?.value;
+  if (
+    value === "resend" ||
+    value === "postmark" ||
+    value === "mailgun" ||
+    value === "sendgrid" ||
+    value === "ses"
+  ) {
+    return value;
+  }
+  return "smtp";
 }
 }
 
 
+export async function getFromAddress(): Promise<string> {
+  const provider = await getEmailProvider();
+
+  const keyMap: Record<
+    EmailProvider,
+    { email: string; name: string }
+  > = {
+    smtp: {
+      email: SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.SMTP_FROM_NAME,
+    },
+    resend: {
+      email: SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.RESEND_FROM_NAME,
+    },
+    postmark: {
+      email: SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME,
+    },
+    mailgun: {
+      email: SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME,
+    },
+    sendgrid: {
+      email: SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME,
+    },
+    ses: {
+      email: SYSTEM_SETTING_KEYS.SES_FROM_EMAIL,
+      name: SYSTEM_SETTING_KEYS.SES_FROM_NAME,
+    },
+  };
+
+  const keys = keyMap[provider];
+  const rows = await db.systemSetting.findMany({
+    where: { key: { in: [keys.email, keys.name] } },
+  });
+  const map = new Map(rows.map((r) => [r.key, r.value]));
+
+  const fromEmail =
+    map.get(keys.email) || process.env.SMTP_FROM_EMAIL || "noreply@example.com";
+  const fromName = map.get(keys.name) || "Torqvoice";
+
+  return `${fromName} <${fromEmail}>`;
+}
+
+// ─── SMTP ──────────────────────────────────────────────────────────────────
+
 async function getSmtpSettings() {
 async function getSmtpSettings() {
   const keys = [
   const keys = [
     SYSTEM_SETTING_KEYS.SMTP_HOST,
     SYSTEM_SETTING_KEYS.SMTP_HOST,
@@ -41,7 +112,6 @@ async function getSmtpSettings() {
 }
 }
 
 
 async function getTransporter() {
 async function getTransporter() {
-  // Read from DB first, fall back to env vars
   const settings = await getSmtpSettings();
   const settings = await getSmtpSettings();
 
 
   const host =
   const host =
@@ -109,6 +179,8 @@ async function sendViaSmtp(options: SendMailOptions) {
   });
   });
 }
 }
 
 
+// ─── Resend ────────────────────────────────────────────────────────────────
+
 async function getResendSettings() {
 async function getResendSettings() {
   const keys = [
   const keys = [
     SYSTEM_SETTING_KEYS.RESEND_API_KEY,
     SYSTEM_SETTING_KEYS.RESEND_API_KEY,
@@ -148,12 +220,214 @@ async function sendViaResend(options: SendMailOptions) {
   });
   });
 }
 }
 
 
+// ─── Postmark ──────────────────────────────────────────────────────────────
+
+async function sendViaPostmark(options: SendMailOptions) {
+  const rows = await db.systemSetting.findMany({
+    where: {
+      key: { in: [SYSTEM_SETTING_KEYS.POSTMARK_API_KEY] },
+    },
+  });
+  const map = new Map(rows.map((r) => [r.key, r.value]));
+  const apiKey = map.get(SYSTEM_SETTING_KEYS.POSTMARK_API_KEY);
+
+  if (!apiKey) {
+    throw new Error(
+      "Postmark is not configured. Add your Server Token in Admin Settings.",
+    );
+  }
+
+  const client = new PostmarkClient(apiKey);
+
+  if (options.attachments?.length) {
+    await client.sendEmail({
+      From: options.from,
+      To: options.to,
+      Subject: options.subject,
+      HtmlBody: options.html,
+      Attachments: options.attachments.map((a) => ({
+        Name: a.filename,
+        Content: a.content.toString("base64"),
+        ContentType: "application/octet-stream",
+        ContentID: "",
+      })),
+    });
+  } else {
+    await client.sendEmail({
+      From: options.from,
+      To: options.to,
+      Subject: options.subject,
+      HtmlBody: options.html,
+    });
+  }
+}
+
+// ─── Mailgun ───────────────────────────────────────────────────────────────
+
+async function sendViaMailgun(options: SendMailOptions) {
+  const keys = [
+    SYSTEM_SETTING_KEYS.MAILGUN_API_KEY,
+    SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN,
+    SYSTEM_SETTING_KEYS.MAILGUN_REGION,
+  ];
+  const rows = await db.systemSetting.findMany({
+    where: { key: { in: keys } },
+  });
+  const map = new Map(rows.map((r) => [r.key, r.value]));
+
+  const apiKey = map.get(SYSTEM_SETTING_KEYS.MAILGUN_API_KEY);
+  const domain = map.get(SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN);
+  const region = map.get(SYSTEM_SETTING_KEYS.MAILGUN_REGION) || "us";
+
+  if (!apiKey || !domain) {
+    throw new Error(
+      "Mailgun is not configured. Add your API key and domain in Admin Settings.",
+    );
+  }
+
+  const mailgun = new Mailgun(FormData);
+  const mg = mailgun.client({
+    username: "api",
+    key: apiKey,
+    url: region === "eu" ? "https://api.eu.mailgun.net" : undefined,
+  });
+
+  await mg.messages.create(domain, {
+    from: options.from,
+    to: [options.to],
+    subject: options.subject,
+    html: options.html,
+    ...(options.attachments?.length && {
+      attachment: options.attachments.map((a) => ({
+        filename: a.filename,
+        data: a.content,
+      })),
+    }),
+  });
+}
+
+// ─── SendGrid ──────────────────────────────────────────────────────────────
+
+async function sendViaSendGrid(options: SendMailOptions) {
+  const rows = await db.systemSetting.findMany({
+    where: {
+      key: { in: [SYSTEM_SETTING_KEYS.SENDGRID_API_KEY] },
+    },
+  });
+  const map = new Map(rows.map((r) => [r.key, r.value]));
+  const apiKey = map.get(SYSTEM_SETTING_KEYS.SENDGRID_API_KEY);
+
+  if (!apiKey) {
+    throw new Error(
+      "SendGrid is not configured. Add your API key in Admin Settings.",
+    );
+  }
+
+  sgMail.setApiKey(apiKey);
+
+  const msg: MailDataRequired = {
+    from: options.from,
+    to: options.to,
+    subject: options.subject,
+    html: options.html,
+  };
+
+  if (options.attachments?.length) {
+    msg.attachments = options.attachments.map((a) => ({
+      filename: a.filename,
+      content: a.content.toString("base64"),
+      type: "application/octet-stream",
+      disposition: "attachment" as const,
+    }));
+  }
+
+  await sgMail.send(msg);
+}
+
+// ─── Amazon SES ────────────────────────────────────────────────────────────
+
+async function sendViaSes(options: SendMailOptions) {
+  const keys = [
+    SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID,
+    SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY,
+    SYSTEM_SETTING_KEYS.SES_REGION,
+  ];
+  const rows = await db.systemSetting.findMany({
+    where: { key: { in: keys } },
+  });
+  const map = new Map(rows.map((r) => [r.key, r.value]));
+
+  const accessKeyId = map.get(SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID);
+  const secretAccessKey = map.get(SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY);
+  const region = map.get(SYSTEM_SETTING_KEYS.SES_REGION) || "us-east-1";
+
+  if (!accessKeyId || !secretAccessKey) {
+    throw new Error(
+      "Amazon SES is not configured. Add your credentials in Admin Settings.",
+    );
+  }
+
+  // Build raw email via nodemailer (supports attachments) then send through SES
+  const transporter = nodemailer.createTransport({ streamTransport: true });
+  const info = await transporter.sendMail({
+    from: options.from,
+    to: options.to,
+    subject: options.subject,
+    html: options.html,
+    attachments: options.attachments?.map((a) => ({
+      filename: a.filename,
+      content: a.content,
+    })),
+  });
+
+  const rawMessage = Buffer.isBuffer(info.message)
+    ? info.message
+    : await streamToBuffer(info.message);
+
+  const client = new SESClient({
+    region,
+    credentials: { accessKeyId, secretAccessKey },
+  });
+
+  await client.send(
+    new SendRawEmailCommand({ RawMessage: { Data: rawMessage } }),
+  );
+}
+
+function streamToBuffer(
+  stream: NodeJS.ReadableStream,
+): Promise<Uint8Array> {
+  return new Promise((resolve, reject) => {
+    const chunks: Buffer[] = [];
+    stream.on("data", (chunk: Buffer) => chunks.push(chunk));
+    stream.on("end", () => resolve(Buffer.concat(chunks)));
+    stream.on("error", reject);
+  });
+}
+
+// ─── Router ────────────────────────────────────────────────────────────────
+
 export async function sendMail(options: SendMailOptions) {
 export async function sendMail(options: SendMailOptions) {
   const provider = await getEmailProvider();
   const provider = await getEmailProvider();
 
 
-  if (provider === "resend") {
-    await sendViaResend(options);
-  } else {
-    await sendViaSmtp(options);
+  switch (provider) {
+    case "resend":
+      await sendViaResend(options);
+      break;
+    case "postmark":
+      await sendViaPostmark(options);
+      break;
+    case "mailgun":
+      await sendViaMailgun(options);
+      break;
+    case "sendgrid":
+      await sendViaSendGrid(options);
+      break;
+    case "ses":
+      await sendViaSes(options);
+      break;
+    default:
+      await sendViaSmtp(options);
+      break;
   }
   }
 }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов