recator placement of code

This commit is contained in:
2026-02-17 11:46:57 -06:00
parent 31f8c368d9
commit 23c000fa7f
77 changed files with 4528 additions and 2697 deletions

143
backend/utils/auth.utils.ts Normal file
View 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));
// },
},
});

View 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");

View 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",
"*",
],
});
};

View 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();
}
};

View 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 doesnt 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>

View 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>

View 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>

View 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);
}

View 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,
});
}
};

View 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 };
}
}

View 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,
});
});
};