feat(auth): added in the intital setup auth
This commit is contained in:
@@ -1,30 +1,29 @@
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import express from "express";
|
||||
import morgan from "morgan";
|
||||
import { createLogger } from "./src/logger/logger.controller.js";
|
||||
import { connectProdSql } from "./src/prodSql/prodSqlConnection.controller.js";
|
||||
import { setupRoutes } from "./src/routeHandler.routes.js";
|
||||
import { auth } from "./src/utils/auth.utils.js";
|
||||
import { lstCors } from "./src/utils/cors.utils.js";
|
||||
|
||||
const port = Number(process.env.PORT);
|
||||
|
||||
const startApp = async () => {
|
||||
const createApp = async () => {
|
||||
const log = createLogger({ module: "system", subModule: "main start" });
|
||||
const app = express();
|
||||
let baseUrl = "/";
|
||||
|
||||
// global env that run only in dev
|
||||
if (process.env.NODE_ENV?.trim() !== "production") {
|
||||
app.use(morgan("tiny"));
|
||||
baseUrl = "/lst";
|
||||
}
|
||||
|
||||
// start the connection to the prod sql server
|
||||
connectProdSql();
|
||||
|
||||
app.set("trust proxy", true);
|
||||
app.all(`${baseUrl}api/auth/*splat`, toNodeHandler(auth));
|
||||
app.use(express.json());
|
||||
app.use(lstCors());
|
||||
setupRoutes(baseUrl, app);
|
||||
|
||||
app.listen(port, () => {
|
||||
log.info(`Listening on port ${port}`);
|
||||
});
|
||||
log.info("Express app created");
|
||||
return { app, baseUrl };
|
||||
};
|
||||
|
||||
startApp();
|
||||
export default createApp;
|
||||
|
||||
22
backend/server.ts
Normal file
22
backend/server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import os from "node:os";
|
||||
import createApp from "./app.js";
|
||||
import { createLogger } from "./src/logger/logger.controller.js";
|
||||
import { connectProdSql } from "./src/prodSql/prodSqlConnection.controller.js";
|
||||
|
||||
const port = Number(process.env.PORT);
|
||||
|
||||
const start = async () => {
|
||||
const log = createLogger({ module: "system", subModule: "main start" });
|
||||
|
||||
connectProdSql();
|
||||
|
||||
const { app, baseUrl } = await createApp();
|
||||
|
||||
app.listen(port, async () => {
|
||||
log.info(
|
||||
`Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
start();
|
||||
157
backend/src/db/schema/auth.schema.ts
Normal file
157
backend/src/db/schema/auth.schema.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
role: text("role"),
|
||||
banned: boolean("banned").default(false),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
username: text("username").unique(),
|
||||
displayUsername: text("display_username"),
|
||||
lastLogin: timestamp("last_login").defaultNow(),
|
||||
});
|
||||
|
||||
export const session = pgTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const account = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
);
|
||||
|
||||
export const jwks = pgTable("jwks", {
|
||||
id: text("id").primaryKey(),
|
||||
publicKey: text("public_key").notNull(),
|
||||
privateKey: text("private_key").notNull(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
});
|
||||
|
||||
export const apikey = pgTable(
|
||||
"apikey",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name"),
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
enabled: boolean("enabled").default(true),
|
||||
rateLimitEnabled: boolean("rate_limit_enabled").default(true),
|
||||
rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000),
|
||||
rateLimitMax: integer("rate_limit_max").default(10),
|
||||
requestCount: integer("request_count").default(0),
|
||||
remaining: integer("remaining"),
|
||||
lastRequest: timestamp("last_request"),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
permissions: text("permissions"),
|
||||
metadata: text("metadata"),
|
||||
},
|
||||
(table) => [
|
||||
index("apikey_key_idx").on(table.key),
|
||||
index("apikey_userId_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
apikeys: many(apikey),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [apikey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
@@ -18,7 +18,7 @@ export const logs = pgTable("logs", {
|
||||
stack: jsonb("stack").default([]),
|
||||
checked: boolean("checked").default(false),
|
||||
hostname: text("hostname"),
|
||||
createdAt: timestamp("createdAt").defaultNow(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const logSchema = createSelectSchema(logs);
|
||||
|
||||
87
backend/src/utils/auth.utils.ts
Normal file
87
backend/src/utils/auth.utils.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { betterAuth, type User } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { admin, apiKey, jwt, username } from "better-auth/plugins";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db/db.controller.js";
|
||||
import * as rawSchema from "../db/schema/auth.schema.js";
|
||||
import { allowedOrigins } from "./cors.utils.js";
|
||||
import { sendEmail } from "./sendEmail.utils.js";
|
||||
|
||||
export const schema = {
|
||||
user: rawSchema.user,
|
||||
session: rawSchema.session,
|
||||
account: rawSchema.account,
|
||||
verification: rawSchema.verification,
|
||||
jwks: rawSchema.jwks,
|
||||
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
||||
};
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "lst",
|
||||
baseURL: process.env.URL,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
}),
|
||||
plugins: [
|
||||
jwt({ jwt: { expirationTime: "1h" } }),
|
||||
apiKey(),
|
||||
admin(),
|
||||
username(),
|
||||
],
|
||||
trustedOrigins: allowedOrigins,
|
||||
// email or username and password.
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8, // optional config
|
||||
resetPasswordTokenExpirySeconds: process.env.RESET_EXPIRY_SECONDS, // time in seconds
|
||||
sendResetPassword: async ({ user, token }) => {
|
||||
const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`;
|
||||
const expiryMinutes = Math.floor(
|
||||
parseInt(process.env.RESET_EXPIRY_SECONDS ?? "3600", 10) / 60,
|
||||
);
|
||||
const expiryText =
|
||||
expiryMinutes >= 60
|
||||
? `${expiryMinutes / 60} hour${expiryMinutes === 60 ? "" : "s"}`
|
||||
: `${expiryMinutes} minutes`;
|
||||
const emailData = {
|
||||
email: user.email,
|
||||
subject: "LST- Forgot password request",
|
||||
template: "forgotPassword",
|
||||
context: {
|
||||
username: user.name,
|
||||
email: user.email,
|
||||
url: frontendUrl,
|
||||
expiry: expiryText,
|
||||
},
|
||||
};
|
||||
await sendEmail(emailData);
|
||||
},
|
||||
// onPasswordReset: async ({ user }, request) => {
|
||||
// // your logic here
|
||||
// console.log(`Password for user ${user.email} has been reset.`);
|
||||
// },
|
||||
},
|
||||
session: {
|
||||
expiresIn: 60 * 60,
|
||||
updateAge: 60 * 5,
|
||||
freshAge: 60 * 2,
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60,
|
||||
},
|
||||
},
|
||||
cookie: {
|
||||
path: "/lst/app",
|
||||
sameSite: "lax",
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
},
|
||||
events: {
|
||||
async onSignInSuccess({ user }: { user: User }) {
|
||||
await db
|
||||
.update(rawSchema.user)
|
||||
.set({ lastLogin: new Date() })
|
||||
.where(eq(schema.user.id, user.id));
|
||||
},
|
||||
},
|
||||
});
|
||||
53
backend/src/utils/cors.utils.ts
Normal file
53
backend/src/utils/cors.utils.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import cors from "cors";
|
||||
|
||||
export const allowedOrigins = [
|
||||
"*.alpla.net",
|
||||
"http://localhost:4173",
|
||||
"http://localhost:4200",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:4000",
|
||||
"http://localhost:4001",
|
||||
"http://localhost:5500",
|
||||
`${process.env.URL}`,
|
||||
];
|
||||
export const lstCors = () => {
|
||||
return cors({
|
||||
origin: (origin, callback) => {
|
||||
//console.log("CORS request from origin:", origin);
|
||||
|
||||
if (!origin) return callback(null, true); // allow same-site or direct calls
|
||||
|
||||
try {
|
||||
const hostname = new URL(origin).hostname; // strips protocol/port
|
||||
//console.log("Parsed hostname:", hostname);
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Now this works for *.alpla.net
|
||||
if (hostname.endsWith(".alpla.net") || hostname === "alpla.net") {
|
||||
return callback(null, true);
|
||||
}
|
||||
} catch (_) {
|
||||
//console.error("Invalid Origin header:", origin);
|
||||
}
|
||||
|
||||
return callback(new Error(`Not allowed by CORS: ${origin}`));
|
||||
},
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
credentials: true,
|
||||
exposedHeaders: ["set-cookie", "expo-protocol-version", "expo-sfv-version"],
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"X-Requested-With",
|
||||
"XMLHttpRequest",
|
||||
"expo-runtime-version",
|
||||
"expo-platform",
|
||||
"expo-channel-name",
|
||||
"*",
|
||||
],
|
||||
});
|
||||
};
|
||||
69
backend/src/utils/mailViews/forgotPassword.hbs
Normal file
69
backend/src/utils/mailViews/forgotPassword.hbs
Normal file
@@ -0,0 +1,69 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Password Reset</title>
|
||||
{{> styles}}
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
}
|
||||
h2 {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
background-color: #4f46e5;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.footer {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<h2>Password Reset Request</h2>
|
||||
<p>Hi {{username}},</p>
|
||||
|
||||
<p>We received a request to reset the password for your account ({{email}}). If this was you, you can create a new password by clicking the button below:</p>
|
||||
|
||||
<p style="text-align:center;">
|
||||
<a href="{{url}}" class="btn">Reset Your Password</a>
|
||||
</p>
|
||||
|
||||
<p>If the button above doesn’t work, copy and paste the following link into your web browser:</p>
|
||||
<p style="word-break: break-all; color:#4f46e5;">{{url}}</p>
|
||||
|
||||
<p><strong>Note:</strong> This link will expire in <b>{{expiry}}</b> for your security.</p>
|
||||
|
||||
<p>If you did not request a password reset, you can safely ignore this email — your account will remain secure.</p>
|
||||
|
||||
<p>Thanks,<br/>The LST Team</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>You are receiving this email because a password reset was requested for your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
backend/src/utils/mailViews/styles.hbs
Normal file
6
backend/src/utils/mailViews/styles.hbs
Normal file
@@ -0,0 +1,6 @@
|
||||
<style>
|
||||
table { width: 100%; background-color: #ffffff; border-collapse: collapse;
|
||||
border-width: 2px; border-color: #14BDEA; border-style: solid; color:
|
||||
#000000; } th, td { border: 1px solid #ddd; padding: 8px; } th {
|
||||
background-color: #f4f4f4; }
|
||||
</style>
|
||||
33
backend/src/utils/mailViews/testEmail.hbs
Normal file
33
backend/src/utils/mailViews/testEmail.hbs
Normal file
@@ -0,0 +1,33 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Order Summary</title>
|
||||
{{> styles}}
|
||||
{{!-- <link rel="stylesheet" href="styles/styles.css" /> --}}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{name}}, This is a test that the emailing actually works as intended.</h1>
|
||||
<p>All,
|
||||
This is an example of the test email
|
||||
</p>
|
||||
{{!-- <table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item Name</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each items}}
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<td>{{quantity}}</td>
|
||||
<td>{{price}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table> --}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,8 +3,8 @@ import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
interface Data {
|
||||
success: boolean;
|
||||
module: "system" | "ocp" | "routes" | "datamart";
|
||||
subModule: "db" | "labeling" | "printer" | "prodSql" | "query";
|
||||
module: "system" | "ocp" | "routes" | "datamart" | "utils";
|
||||
subModule: "db" | "labeling" | "printer" | "prodSql" | "query" | "sendmail";
|
||||
level: "info" | "error" | "debug" | "fatal";
|
||||
message: string;
|
||||
data?: unknown[];
|
||||
@@ -47,6 +47,9 @@ export const returnFunc = (data: Data) => {
|
||||
return {
|
||||
success: data.success,
|
||||
message: data.message,
|
||||
level: data.level,
|
||||
module: data.module,
|
||||
subModule: data.subModule,
|
||||
data: data.data || [],
|
||||
};
|
||||
};
|
||||
|
||||
125
backend/src/utils/sendEmail.utils.ts
Normal file
125
backend/src/utils/sendEmail.utils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { Transporter } from "nodemailer";
|
||||
import nodemailer from "nodemailer";
|
||||
import type Mail from "nodemailer/lib/mailer/index.js";
|
||||
import type { Address } from "nodemailer/lib/mailer/index.js";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
|
||||
import hbs from "nodemailer-express-handlebars";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promisify } from "util";
|
||||
import { returnFunc } from "./returnHelper.utils.js";
|
||||
import { tryCatch } from "./trycatch.utils.js";
|
||||
|
||||
interface HandlebarsMailOptions extends Mail.Options {
|
||||
template: string;
|
||||
context: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EmailData {
|
||||
email: string;
|
||||
subject: string;
|
||||
template: string;
|
||||
context: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const sendEmail = async (data: EmailData) => {
|
||||
let transporter: Transporter;
|
||||
let fromEmail: string | Address;
|
||||
|
||||
if (os.hostname().includes("OLP")) {
|
||||
transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD, // The 16-character App Password
|
||||
},
|
||||
//debug: true,
|
||||
});
|
||||
|
||||
// update the from email
|
||||
fromEmail = process.env.EMAIL_USER as string;
|
||||
|
||||
// update the from email
|
||||
fromEmail = `noreply@alpla.com`;
|
||||
} else {
|
||||
//create the servers smtp config
|
||||
let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`;
|
||||
|
||||
if (os.hostname().includes("VMS036")) {
|
||||
host = "USMCD1-smtp.alpla.net";
|
||||
}
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: 25,
|
||||
rejectUnauthorized: false,
|
||||
//secure: false,
|
||||
// auth: {
|
||||
// user: "alplaprod",
|
||||
// pass: "obelix",
|
||||
// },
|
||||
debug: true,
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
// update the from email
|
||||
fromEmail = `noreply@alpla.com`;
|
||||
}
|
||||
|
||||
// create the handlebars view
|
||||
const viewPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"./mailViews",
|
||||
);
|
||||
|
||||
const handlebarOptions = {
|
||||
viewEngine: {
|
||||
extname: ".hbs",
|
||||
defaultLayout: "",
|
||||
partialsDir: viewPath,
|
||||
},
|
||||
viewPath: viewPath,
|
||||
extName: ".hbs",
|
||||
};
|
||||
|
||||
transporter.use("compile", hbs(handlebarOptions));
|
||||
|
||||
const mailOptions: HandlebarsMailOptions = {
|
||||
from: fromEmail,
|
||||
to: data.email,
|
||||
subject: data.subject,
|
||||
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
||||
//html: emailTemplate("Blake's Test", "This is an example with css"),
|
||||
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||
context: data.context,
|
||||
};
|
||||
|
||||
// now verify and send the email
|
||||
const sendMailPromise = promisify(transporter.sendMail).bind(transporter);
|
||||
|
||||
const { data: mail, error } = await tryCatch(sendMailPromise(mailOptions));
|
||||
|
||||
if (mail) {
|
||||
return returnFunc({
|
||||
success: true,
|
||||
level: "info",
|
||||
module: "utils",
|
||||
subModule: "sendmail",
|
||||
message: `Email was sent to: ${data.email}`,
|
||||
data: [],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return returnFunc({
|
||||
success: false,
|
||||
level: "error",
|
||||
module: "utils",
|
||||
subModule: "sendmail",
|
||||
message: `Error sending Email to : ${data.email}`,
|
||||
data: [{ error: error }],
|
||||
notify: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user