recator placement of code
This commit is contained in:
143
backend/utils/auth.utils.ts
Normal file
143
backend/utils/auth.utils.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import {
|
||||
admin,
|
||||
apiKey,
|
||||
createAuthMiddleware,
|
||||
customSession,
|
||||
jwt,
|
||||
lastLoginMethod,
|
||||
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",
|
||||
schema,
|
||||
}),
|
||||
user: {
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
lastLogin: {
|
||||
type: "date",
|
||||
required: true,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
jwt({ jwt: { expirationTime: "1h" } }),
|
||||
apiKey(),
|
||||
admin(),
|
||||
lastLoginMethod(),
|
||||
username({
|
||||
minUsernameLength: 5,
|
||||
usernameValidator: (username) => {
|
||||
if (username === "admin") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
customSession(async ({ user, session }) => {
|
||||
const roles = await db
|
||||
.select({ roles: rawSchema.user.role })
|
||||
.from(rawSchema.user)
|
||||
.where(eq(rawSchema.user.id, session.id));
|
||||
return {
|
||||
roles,
|
||||
user: {
|
||||
...user,
|
||||
//newField: "newField",
|
||||
},
|
||||
session,
|
||||
};
|
||||
}),
|
||||
],
|
||||
trustedOrigins: allowedOrigins,
|
||||
|
||||
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,
|
||||
},
|
||||
hooks: {
|
||||
after: createAuthMiddleware(async (ctx) => {
|
||||
if (ctx.path.startsWith("/login")) {
|
||||
const newSession = ctx.context.newSession;
|
||||
if (newSession) {
|
||||
// something here later
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
events: {
|
||||
// async onSignInSuccess({ user }: { user: User }) {
|
||||
// await db
|
||||
// .update(rawSchema.user)
|
||||
// .set({ lastLogin: new Date() })
|
||||
// .where(eq(schema.user.id, user.id));
|
||||
// },
|
||||
},
|
||||
});
|
||||
83
backend/utils/checkHost.utils.ts
Normal file
83
backend/utils/checkHost.utils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import dns from "node:dns";
|
||||
import net from "node:net";
|
||||
|
||||
type IP = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
export const checkHostnamePort = async (host: string): Promise<boolean> => {
|
||||
const [hostname, port] = host.split(":");
|
||||
|
||||
// a just incase to make sure we have the host name and port
|
||||
if (!hostname || !port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the hostname to an IP address
|
||||
const ip = (await checkHostUp(hostname)) as IP;
|
||||
|
||||
if (!ip.success) {
|
||||
return false;
|
||||
}
|
||||
const portNum = parseInt(port, 10);
|
||||
|
||||
const hostUp = (await pingHost(hostname, portNum)) as IP;
|
||||
|
||||
if (!hostUp.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const pingHost = (host: string, port: number) => {
|
||||
return new Promise((res) => {
|
||||
const s = new net.Socket();
|
||||
|
||||
s.setTimeout(2000);
|
||||
|
||||
s.connect(port, host);
|
||||
|
||||
s.on("connect", () => {
|
||||
s.destroy();
|
||||
res({
|
||||
success: true,
|
||||
message: "Server up",
|
||||
});
|
||||
});
|
||||
|
||||
s.on("timeout", () => {
|
||||
s.destroy();
|
||||
res({
|
||||
success: true,
|
||||
message: "Server Offline or unreachable, please check connection",
|
||||
});
|
||||
});
|
||||
|
||||
s.on("error", (e) => {
|
||||
s.destroy();
|
||||
return res({
|
||||
success: false,
|
||||
message: "Encountered error while checking if host:port up",
|
||||
error: e,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const checkHostUp = (host: string) => {
|
||||
return new Promise((res) => {
|
||||
dns.lookup(host, (err, address) => {
|
||||
if (err) {
|
||||
return res({
|
||||
success: false,
|
||||
message: "Error connecting to the server",
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
res({ success: true, message: "Server Ip", data: address });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//checkHostnamePort("usmcd1vms036:1433");
|
||||
55
backend/utils/cors.utils.ts
Normal file
55
backend/utils/cors.utils.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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",
|
||||
"https://admin.socket.io",
|
||||
"https://electron-socket-io-playground.vercel.app",
|
||||
`${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",
|
||||
"*",
|
||||
],
|
||||
});
|
||||
};
|
||||
75
backend/utils/croner.utils.ts
Normal file
75
backend/utils/croner.utils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Cron } from "croner";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
// example createJob
|
||||
// createCronJob("test Cron", "*/5 * * * * *", async () => {
|
||||
// console.log("help");
|
||||
// });
|
||||
|
||||
export interface JobInfo {
|
||||
name: string;
|
||||
schedule: string;
|
||||
nextRun: Date | null;
|
||||
isRunning: boolean;
|
||||
}
|
||||
|
||||
// Store running cronjobs
|
||||
export const runningCrons: Record<string, Cron> = {};
|
||||
|
||||
export const createCronJob = async (
|
||||
name: string,
|
||||
schedule: string, // cron string with 8 8 IE: */5 * * * * * every 5th second
|
||||
task?: () => Promise<void>, // what function are we passing over
|
||||
) => {
|
||||
// get the timezone based on the os timezone set
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Destroy existing job if it exists
|
||||
if (runningCrons[name]) {
|
||||
runningCrons[name].stop();
|
||||
}
|
||||
|
||||
// Create new job with Croner
|
||||
runningCrons[name] = new Cron(
|
||||
schedule,
|
||||
{
|
||||
timezone: timeZone,
|
||||
catch: true, // Prevents unhandled rejections
|
||||
name: name,
|
||||
},
|
||||
task,
|
||||
);
|
||||
|
||||
const log = createLogger({ module: "system", subModule: "croner" });
|
||||
|
||||
log.info({}, `A job for ${name} was just created.`);
|
||||
};
|
||||
|
||||
export const getAllJobs = (): JobInfo[] => {
|
||||
return Object.entries(runningCrons).map(([name, job]) => ({
|
||||
name,
|
||||
schedule: job.getPattern() || "invalid",
|
||||
nextRun: job.nextRun() || null,
|
||||
lastRun: job.previousRun() || null,
|
||||
isRunning: job.isRunning(), //job ? !job.isStopped() : false,
|
||||
}));
|
||||
};
|
||||
|
||||
export const removeCronJob = (name: string) => {
|
||||
if (runningCrons[name]) {
|
||||
runningCrons[name].stop();
|
||||
delete runningCrons[name];
|
||||
}
|
||||
};
|
||||
|
||||
export const stopCronJob = (name: string) => {
|
||||
if (runningCrons[name]) {
|
||||
runningCrons[name].pause();
|
||||
}
|
||||
};
|
||||
|
||||
export const resumeCronJob = (name: string) => {
|
||||
if (runningCrons[name]) {
|
||||
runningCrons[name].resume();
|
||||
}
|
||||
};
|
||||
69
backend/utils/mailViews/forgotPassword.hbs
Normal file
69
backend/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/utils/mailViews/styles.hbs
Normal file
6
backend/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/utils/mailViews/testEmail.hbs
Normal file
33
backend/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>
|
||||
79
backend/utils/returnHelper.utils.ts
Normal file
79
backend/utils/returnHelper.utils.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Response } from "express";
|
||||
import { createLogger } from "../logger/logger.controller.js";
|
||||
|
||||
interface Data {
|
||||
success: boolean;
|
||||
module: "system" | "ocp" | "routes" | "datamart" | "utils";
|
||||
subModule:
|
||||
| "db"
|
||||
| "labeling"
|
||||
| "printer"
|
||||
| "prodSql"
|
||||
| "query"
|
||||
| "sendmail"
|
||||
| "auth"
|
||||
| "datamart"
|
||||
| "jobs";
|
||||
level: "info" | "error" | "debug" | "fatal";
|
||||
message: string;
|
||||
data?: unknown[];
|
||||
notify?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This dose the return process and log at the same time, vs needing to log and return at the same time.
|
||||
* When to use.
|
||||
* APIs
|
||||
* actual returns and needing to stop.
|
||||
* Example Data
|
||||
* success:true
|
||||
* module: system | printers | etc
|
||||
* submodule: sql connection | printer test | etc
|
||||
* level "info" | "error" | "debug" | "fatal"
|
||||
* message: "short description of the return"
|
||||
* data: [] the data that will be passed back
|
||||
* notify: false by default this is to send a notification to a users email to alert them of an issue.
|
||||
*/
|
||||
export const returnFunc = (data: Data) => {
|
||||
const notify = data.notify ? data.notify : false;
|
||||
const log = createLogger({ module: data.module, subModule: data.subModule });
|
||||
// handle the logging part
|
||||
switch (data.level) {
|
||||
case "info":
|
||||
log.info({ notify: notify }, data.message);
|
||||
break;
|
||||
case "error":
|
||||
log.error({ notify: notify, error: data.data }, data.message);
|
||||
break;
|
||||
case "debug":
|
||||
log.debug({ notify: notify }, data.message);
|
||||
break;
|
||||
case "fatal":
|
||||
log.fatal({ notify: notify }, data.message);
|
||||
}
|
||||
|
||||
// api section to return
|
||||
return {
|
||||
success: data.success,
|
||||
message: data.message,
|
||||
level: data.level,
|
||||
module: data.module,
|
||||
subModule: data.subModule,
|
||||
data: data.data || [],
|
||||
};
|
||||
};
|
||||
|
||||
export function apiReturn(
|
||||
res: Response,
|
||||
opts: Data & { status?: number },
|
||||
optional?: unknown, // leave this as unknown so we can pass an object or an array over.
|
||||
): Response {
|
||||
const result = returnFunc(opts);
|
||||
const code = opts.status ?? (opts.success ? 200 : 500);
|
||||
if (optional) {
|
||||
return res
|
||||
.status(code ?? (opts.success ? 200 : 500))
|
||||
.json({ ...result, optional });
|
||||
}
|
||||
return res.status(code ?? (opts.success ? 200 : 500)).json(result);
|
||||
}
|
||||
125
backend/utils/sendEmail.utils.ts
Normal file
125
backend/utils/sendEmail.utils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
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 { 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 = `donotreply@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 = `donotreply@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,
|
||||
});
|
||||
}
|
||||
};
|
||||
28
backend/utils/trycatch.utils.ts
Normal file
28
backend/utils/trycatch.utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
type Success<T> = { data: T; error: null };
|
||||
type Failure<E> = { data: null; error: E };
|
||||
|
||||
export type Result<T, E = Error> = Success<T> | Failure<E>;
|
||||
|
||||
/**
|
||||
* A universal tryCatch wrapper that:
|
||||
* - Never throws
|
||||
* - Always resolves to Result<T,E>
|
||||
* - Allows optional error mapping function for strong typing
|
||||
*/
|
||||
|
||||
export async function tryCatch<T, E = Error>(
|
||||
promise: Promise<T>,
|
||||
onError?: (error: unknown) => E,
|
||||
): Promise<Result<T, E>> {
|
||||
try {
|
||||
const data = await promise;
|
||||
return { data, error: null };
|
||||
} catch (err: unknown) {
|
||||
const error = onError
|
||||
? onError(err)
|
||||
: err instanceof Error
|
||||
? (err as E)
|
||||
: (new Error(String(err)) as E);
|
||||
return { data: null, error };
|
||||
}
|
||||
}
|
||||
16
backend/utils/utils.routes.ts
Normal file
16
backend/utils/utils.routes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Express } from "express";
|
||||
import { getAllJobs } from "./croner.utils.js";
|
||||
import { apiReturn } from "./returnHelper.utils.js";
|
||||
export const setupUtilsRoutes = (baseUrl: string, app: Express) => {
|
||||
app.get(`${baseUrl}/api/utils`, (_, res) => {
|
||||
return apiReturn(res, {
|
||||
success: true,
|
||||
level: "info",
|
||||
module: "utils",
|
||||
subModule: "jobs",
|
||||
message: "All current Jobs",
|
||||
data: getAllJobs(),
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user