Skip to content

Commit

Permalink
feat: multi option based email
Browse files Browse the repository at this point in the history
  • Loading branch information
potts99 committed Oct 20, 2024
1 parent 3f6b4a3 commit 574371e
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 253 deletions.
29 changes: 14 additions & 15 deletions apps/api/src/lib/nodemailer/auth/forgot-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,42 @@ export async function forgotPassword(
let mail;
let replyto;

const emails = await prisma.email.findMany();
const email_config = await prisma.email.findFirst();

const resetlink = `${link}/auth/reset-password?token=${token}`;

if (emails.length > 0) {
if (email) {
if (process.env.ENVIRONMENT === "development") {
let testAccount = await nodeMailer.createTestAccount();
mail = nodeMailer.createTransport({
port: 1025,
secure: false, // true for 465, false for other ports
auth: {
user: testAccount.user, // generated ethereal user
pass: testAccount.pass, // generated ethereal password
user: testAccount.user,
pass: testAccount.pass,
},
});
} else {
const email = emails[0];
replyto = email.reply;
replyto = email_config?.reply;
mail = nodeMailer.createTransport({
// @ts-ignore
host: email.host,
port: email.port,
secure: email.port === "465" ? true : false, // true for 465, false for other ports
host: email_config?.host,
port: email_config?.port,
secure: email_config?.port === "465" ? true : false, // true for 465, false for other ports
auth: {
user: email.user, // generated ethereal user
pass: email.pass, // generated ethereal password
user: email_config?.user,
pass: email_config?.pass,
},
});
}

console.log("Sending email to: ", email);

let info = await mail.sendMail({
from: replyto, // sender address
to: email, // list of receivers
subject: `Password Reset Request`, // Subject line
text: `Password Reset Code: ${code}, follow this link to reset your password ${resetlink}`, // plain text body
from: replyto,
to: email,
subject: `Password Reset Request`,
text: `Password Reset Code: ${code}, follow this link to reset your password ${resetlink}`,
html: `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
Expand Down
55 changes: 30 additions & 25 deletions apps/api/src/lib/nodemailer/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,46 @@ const nodemailer = require("nodemailer");
const { google } = require("google-auth-library");
const { ConfidentialClientApplication } = require("@azure/identity");

// Environment variables or configuration
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const REFRESH_TOKEN = process.env.REFRESH_TOKEN;
const TENANT_ID = process.env.TENANT_ID;

export async function createTransportProvider() {
const provider = await prisma.email.findFirst({});

if (CLIENT_ID && CLIENT_SECRET && (REFRESH_TOKEN || TENANT_ID)) {
if (!provider) {
throw new Error("No email provider configured.");
}

if (
provider?.clientId &&
provider?.clientSecret &&
(provider?.refreshToken || provider?.tenantId)
) {
// OAuth2 configuration
if (EMAIL_SERVICE === "gmail") {
if (provider?.serviceType === "gmail") {
// Gmail
const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET);
oAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN });
const oAuth2Client = new google.auth.OAuth2(
provider?.clientId,
provider?.clientSecret
);
oAuth2Client.setCredentials({ refresh_token: provider?.refreshToken });
const accessToken = await oAuth2Client.getAccessToken();

return nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
user: EMAIL_USER,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
refreshToken: REFRESH_TOKEN,
user: provider?.user,
clientId: provider?.clientId,
clientSecret: provider?.clientSecret,
refreshToken: provider?.refreshToken,
accessToken: accessToken.token,
},
});
} else if (EMAIL_SERVICE === "microsoft") {
} else if (provider?.serviceType === "microsoft") {
// Microsoft
const cca = new ConfidentialClientApplication({
auth: {
clientId: CLIENT_ID,
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
clientSecret: CLIENT_SECRET,
clientId: provider?.clientId,
authority: `https://login.microsoftonline.com/${provider?.tenantId}`,
clientSecret: provider?.clientSecret,
},
});

Expand All @@ -50,20 +55,20 @@ export async function createTransportProvider() {
service: "hotmail",
auth: {
type: "OAuth2",
user: EMAIL_USER,
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
user: provider?.user,
clientId: provider?.clientId,
clientSecret: provider?.clientSecret,
accessToken: result.accessToken,
},
});
}
} else if (EMAIL_USER && EMAIL_PASS) {
} else if (provider?.user && provider?.pass) {
// Username/password configuration
return nodemailer.createTransport({
service: EMAIL_SERVICE,
service: provider?.serviceType,
auth: {
user: EMAIL_USER,
pass: EMAIL_PASS,
user: provider?.user,
pass: provider?.pass,
},
});
} else {
Expand Down
25 changes: 25 additions & 0 deletions apps/client/@/shadcn/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react"

import { cn } from "@/shadcn/lib/utils"

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"

export { Input }
24 changes: 24 additions & 0 deletions apps/client/@/shadcn/ui/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/shadcn/lib/utils"

const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }
4 changes: 2 additions & 2 deletions apps/client/layouts/adminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export default function AdminLayout({ children }: any) {
icon: Webhook,
},
{
name: "Outbound Emails",
name: "SMTP Email",
href: "/admin/email",
current: location.pathname === "/admin/email",
icon: Mailbox,
},
{
name: "authentication",
name: "Authentication",
href: "/admin/authentication",
current: location.pathname === "/admin/authentication",
icon: KeyRound,
Expand Down
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.1.0",
Expand Down
Loading

0 comments on commit 574371e

Please sign in to comment.