Compare commits

..

7 Commits

46 changed files with 3899 additions and 99 deletions

1
.gitignore vendored
View File

@@ -193,3 +193,4 @@ controller/docker-compose.yml
controller/Dockerfile
controller/Dockerfile-ignore
controller/docker-compose.yml
controller/index.html

View File

@@ -6,7 +6,7 @@ lstWrapper/publish/LstWrapper.exe
lstWrapper/publish/LstWrapper.pdb
lstWrapper/publish/LstWrapper.runtimeconfig.json
lstWrapper/publish/LstWrapper.staticwebassets.endpoints.json
web.config
lstWrapper/publish/web.config
controller/lst_ctl.exe
controller/.env-example
scripts/update-controller-bumpBuild.ps1

View File

@@ -0,0 +1,21 @@
meta {
name: Request Resetpassword
type: http
seq: 10
}
post {
url: {{url}}/lst/api/user/resetPassword
body: json
auth: inherit
}
body:json {
{
"email": "blake.matthes@alpla.com",
}
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,25 @@
meta {
name: Resetpassword
type: http
seq: 11
}
post {
url: http://localhost:4200/api/auth/reset-password/gCo7OUP6CH2Qu7obhvOrhuo9?callbackURL
body: json
auth: inherit
}
params:query {
callbackURL:
}
body:json {
{
"newPassword": "nova0511"
}
}
settings {
encodeUrl: true
}

View File

@@ -17,6 +17,7 @@ import cors from "cors";
import { sendNotify } from "./src/pkg/utils/notify.js";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./src/pkg/auth/auth.js";
import { apiHitMiddleware } from "./src/pkg/middleware/apiHits.js";
const main = async () => {
const env = validateEnv(process.env);
@@ -74,6 +75,8 @@ const main = async () => {
}
// global middleware
app.set("trust proxy", true);
app.use(apiHitMiddleware);
app.all(basePath + "/api/auth/*splat", toNodeHandler(auth)); // sign-in sign-out
app.use(express.json());
@@ -81,6 +84,7 @@ const main = async () => {
"http://localhost:5173", // lstV2 dev
"http://localhost:5500", // lst dev
"http://localhost:4200", // express
"http://localhost:4000", // prod port
env.BETTER_AUTH_URL, // prod
];

View File

@@ -0,0 +1,38 @@
import { Router, type Request, type Response } from "express";
import { authClient } from "../../../pkg/auth/auth-client.js";
import z from "zod";
import { auth } from "../../../pkg/auth/auth.js";
const resetSchema = z.object({
email: z.string(),
});
const router = Router();
router.post("/", async (req: Request, res: Response) => {
try {
const validated = resetSchema.parse(req.body);
const data = await auth.api.requestPasswordReset({
body: {
email: validated.email,
redirectTo: `${process.env.BETTER_AUTH_URL}/user/resetpassword`,
},
});
return res.json(data);
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
return res.status(400).json({
error: "Validation failed",
details: flattened,
});
}
console.log(err);
return res.status(400).json({ error: err });
}
});
export default router;

View File

@@ -2,10 +2,12 @@ import type { Express, Request, Response } from "express";
import me from "./me.js";
import register from "./register.js";
import userRoles from "./userroles.js";
import resetPassword from "./resetPassword.js";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
export const setupAuthRoutes = (app: Express, basePath: string) => {
app.use(basePath + "/api/user/me", requireAuth(), me);
app.use(basePath + "/api/user/resetpassword", resetPassword);
app.use(basePath + "/api/user/register", register);
app.use(basePath + "/api/user/roles", requireAuth(), userRoles);
};

View File

@@ -9,7 +9,7 @@ import {
import type { auth } from "./auth.js";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
baseURL: process.env.BETTER_AUTH_URL,
plugins: [
inferAdditionalFields<typeof auth>(),
usernameClient(),

View File

@@ -5,6 +5,7 @@ import { betterAuth } from "better-auth";
import * as rawSchema from "../db/schema/auth-schema.js";
import type { User } from "better-auth/types";
import { eq } from "drizzle-orm";
import { sendEmail } from "../utils/mail/sendMail.js";
export const schema = {
user: rawSchema.user,
@@ -14,6 +15,7 @@ export const schema = {
jwks: rawSchema.jwks,
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
};
const RESET_EXPIRY_SECONDS = 3600; // 1 hour
export const auth = betterAuth({
database: drizzleAdapter(db, {
@@ -24,11 +26,40 @@ export const auth = betterAuth({
"*.alpla.net",
"http://localhost:5173",
"http://localhost:5500",
"http://localhost:4200",
"http://localhost:4000",
],
appName: "lst",
emailAndPassword: {
enabled: true,
minPasswordLength: 8, // optional config
resetPasswordTokenExpirySeconds: 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(RESET_EXPIRY_SECONDS / 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.`);
// },
},
plugins: [
//jwt({ jwt: { expirationTime: "1h" } }),

View File

@@ -0,0 +1,35 @@
import {
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod";
export const apiHits = pgTable(
"apiHits",
{
apiHit_id: uuid("apiHit_id").defaultRandom().primaryKey(),
method: text("method").notNull(),
path: text("path").notNull(),
body: jsonb("body"),
status: integer("status"),
ip: text("ip"),
duration: integer("duration"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
// uniqueIndex("endpoint").on(table.endpoint, table.ip),
]
);
// Schema for inserting a user - can be used to validate API requests
// export const insertRolesSchema = createInsertSchema(roles, {
// name: z.string().min(3, {message: "Role name must be more than 3 letters"}),
// });
// Schema for selecting a Expenses - can be used to validate API responses
export const selectRolesSchema = createSelectSchema(apiHits);

View File

@@ -0,0 +1,48 @@
import type { Request, Response, NextFunction } from "express";
import { db } from "../db/db.js";
import { apiHits } from "../db/schema/apiHits.js";
type StripPasswords<T> = T extends Array<infer U>
? StripPasswords<U>[]
: T extends object
? { [K in keyof T as Exclude<K, "password">]: StripPasswords<T[K]> }
: T;
function stripPasswords<T>(input: T): StripPasswords<T> {
if (Array.isArray(input)) {
return input.map(stripPasswords) as StripPasswords<T>;
} else if (input && typeof input === "object") {
const entries = Object.entries(input as object)
.filter(([key]) => key !== "password")
.map(([key, value]) => [key, stripPasswords(value)]);
return Object.fromEntries(entries) as StripPasswords<T>;
}
return input as StripPasswords<T>;
}
export function apiHitMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const start = Date.now();
// When response is done sending, log the request + response info
res.on("finish", async () => {
try {
await db.insert(apiHits).values({
method: req.method,
path: req.originalUrl,
body: JSON.stringify(req.body ? stripPasswords(req.body) : {}),
status: res.statusCode,
ip: req.ip,
duration: Date.now() - start,
createdAt: new Date(),
});
} catch (err) {
console.error("Failed to insert apiTracking log:", err);
}
});
next();
}

View File

@@ -0,0 +1,123 @@
import type { Address } from "nodemailer/lib/mailer/index.js";
import type { Transporter } from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
import type Mail from "nodemailer/lib/mailer/index.js";
import os from "os";
import nodemailer from "nodemailer";
import path from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
import hbs from "nodemailer-express-handlebars";
import { createLogger } from "../../logger/logger.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): Promise<any> => {
const log = createLogger({ module: "pkg", subModule: "sendMail" });
let transporter: Transporter;
let fromEmail: string | Address;
if (
os.hostname().includes("OLP") &&
process.env.EMAIL_USER &&
process.env.EMAIL_PASSWORD
) {
transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD,
},
//debug: true,
});
// update the from email
fromEmail = process.env.EMAIL_USER;
} else {
// convert to the correct plant token.
let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`;
//const testServers = ["vms036", "VMS036"];
if (os.hostname().includes("VMS036")) {
host = "USMCD1-smtp.alpla.net";
}
// if (plantToken[0].value === "usiow2") {
// host = "USIOW1-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`;
}
// creating the handlbar options
const viewPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"./views/"
);
const handlebarOptions = {
viewEngine: {
extname: ".hbs",
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
defaultLayout: "", // Specify the default layout
partialsDir: viewPath,
},
viewPath: viewPath,
extName: ".hbs", // File extension for Handlebars templates
};
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("BlakesTest", "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);
try {
// Send email and await the result
const info = await sendMailPromise(mailOptions);
log.info(null, `Email was sent to: ${data.email}`);
return { success: true, message: "Email sent.", data: info };
} catch (err) {
console.log(err);
log.error(
{ error: err },
`Error sending Email to : ${data.email}`
);
return { success: false, message: "Error sending email.", error: err };
}
};

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

@@ -79,6 +79,7 @@
<button id="USBET1VMS006">USBET1VMS006</button>
<button id="USBOW1VMS006">USBOW1VMS006</button>
<button id="USKSC1VMS006">USKSC1VMS006</button>
<button id="USMCD1VMS006">USMCD1VMS006</button>
<button id="USSLC1VMS006">USSLC1VMS006</button>
<button id="USSTP1VMS006">USSTP1VMS006</button>
<button id="USDAY1VMS006">USDAY1VMS006</button>
@@ -266,6 +267,14 @@
}); // "frontend" = payload target
logMessage("info", "Copying to USDAY1VMS006");
};
document.getElementById("USMCD1VMS006").onclick = () => {
socket.emit("update", {
action: "copy",
target: "USMCD1VMS006",
drive: "e",
}); // "frontend" = payload target
logMessage("info", "Copying to USMCD1VMS006");
};
socket.on("logs", (msg) => logMessage("logs", msg));
socket.on("errors", (msg) => logMessage("errors", msg));

View File

@@ -1,4 +1,5 @@
import { Server, Settings, User, type LucideIcon } from "lucide-react";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { userAccess, type UserRoles } from "../../lib/authClient";
import {
SidebarGroup,
@@ -65,6 +66,10 @@ export default function Admin() {
</>
</SidebarMenuItem>
))}
{userAccess(null, ["systemAdmin"]) && (
<TanStackRouterDevtools position="bottom-right" />
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>

View File

@@ -15,16 +15,16 @@ export function Header() {
<SidebarMenuButton size="lg" asChild>
<div className="flex flex-row">
<img
src={"imgs/dkLst.png"}
src={"/lst/app/imgs/dkLst.png"}
alt="Description"
className="size-8"
/>
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-semibold">
Logistics Support Tool
</span>
<span className="">v2.0.0</span>
<span className=""></span>
</div>
</div>
</SidebarMenuButton>

View File

@@ -21,16 +21,17 @@ export default function Nav() {
<div className="m-1">
<ModeToggle />
</div>
{/* <div className="mr-1 ml-1">
{settings.length > 0 && (
<div className="m-1">
<Button>
<a
href={`https://${server[0].value}.alpla.net/lst/d`}
href={`${window.location.origin}/lst/d`}
target="_blank"
>
LST - Docs |
LST - Docs
</a>
)}
</div> */}
</Button>
</div>
{session ? (
<div className="m-1">
<DropdownMenu>

View File

@@ -1,4 +1,9 @@
import { Sidebar, SidebarFooter, SidebarTrigger } from "../ui/sidebar";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarTrigger,
} from "../ui/sidebar";
import { Header } from "./Header";
import Admin from "./Admin";
import { userAccess } from "../../lib/authClient";
@@ -8,7 +13,9 @@ export default function SideBarNav() {
<div className="flex min-h-screen">
<Sidebar collapsible="icon">
<Header />
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
<SidebarContent>
{userAccess(null, ["systemAdmin", "admin"]) && <Admin />}
</SidebarContent>
<SidebarFooter>
<SidebarTrigger />

View File

@@ -0,0 +1,52 @@
import { useState } from "react";
import { useFieldContext } from "..";
import { FieldErrors } from "./FieldErrors";
import { Eye, EyeOff } from "lucide-react";
import { Label } from "../../../components/ui/label";
import { Input } from "../../../components/ui/input";
import { Button } from "../../../components/ui/button";
type PasswordInputProps = {
label: string;
required?: boolean;
};
export const InputPasswordField = ({
label,
required = false,
}: PasswordInputProps) => {
const field = useFieldContext<any>();
const [show, setShow] = useState(false);
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<div className="relative">
<Input
id={field.name}
type={show ? "text" : "password"}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
required={required}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShow((p) => !p)}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
tabIndex={-1} // prevent messing with tab order
>
{show ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -3,12 +3,18 @@ import { SubmitButton } from "./components/SubmitButton";
import { InputField } from "./components/InputField";
import { SelectField } from "./components/SelectField";
import { CheckboxField } from "./components/CheckBox";
import { InputPasswordField } from "./components/InputPasswordField";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
export const { useAppForm } = createFormHook({
fieldComponents: { InputField, SelectField, CheckboxField },
fieldComponents: {
InputField,
InputPasswordField,
SelectField,
CheckboxField,
},
formComponents: { SubmitButton },
fieldContext,
formContext,

View File

@@ -15,6 +15,8 @@ import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users'
import { Route as AdminLayoutAdminSettingsRouteImport } from './routes/_adminLayout/admin/settings'
import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers'
import { Route as authUserSignupRouteImport } from './routes/(auth)/user/signup'
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user/resetpassword'
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
id: '/_adminLayout',
@@ -46,10 +48,22 @@ const AdminLayoutAdminServersRoute = AdminLayoutAdminServersRouteImport.update({
path: '/admin/servers',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const authUserSignupRoute = authUserSignupRouteImport.update({
id: '/(auth)/user/signup',
path: '/user/signup',
getParentRoute: () => rootRouteImport,
} as any)
const authUserResetpasswordRoute = authUserResetpasswordRouteImport.update({
id: '/(auth)/user/resetpassword',
path: '/user/resetpassword',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/login': typeof authLoginRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
'/admin/servers': typeof AdminLayoutAdminServersRoute
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
'/admin/users': typeof AdminLayoutAdminUsersRoute
@@ -57,6 +71,8 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof authLoginRoute
'/user/resetpassword': typeof authUserResetpasswordRoute
'/user/signup': typeof authUserSignupRoute
'/admin/servers': typeof AdminLayoutAdminServersRoute
'/admin/settings': typeof AdminLayoutAdminSettingsRoute
'/admin/users': typeof AdminLayoutAdminUsersRoute
@@ -66,6 +82,8 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
'/(auth)/login': typeof authLoginRoute
'/(auth)/user/resetpassword': typeof authUserResetpasswordRoute
'/(auth)/user/signup': typeof authUserSignupRoute
'/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRoute
'/_adminLayout/admin/settings': typeof AdminLayoutAdminSettingsRoute
'/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute
@@ -75,16 +93,27 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/login'
| '/user/resetpassword'
| '/user/signup'
| '/admin/servers'
| '/admin/settings'
| '/admin/users'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/login' | '/admin/servers' | '/admin/settings' | '/admin/users'
to:
| '/'
| '/login'
| '/user/resetpassword'
| '/user/signup'
| '/admin/servers'
| '/admin/settings'
| '/admin/users'
id:
| '__root__'
| '/'
| '/_adminLayout'
| '/(auth)/login'
| '/(auth)/user/resetpassword'
| '/(auth)/user/signup'
| '/_adminLayout/admin/servers'
| '/_adminLayout/admin/settings'
| '/_adminLayout/admin/users'
@@ -94,6 +123,8 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
authLoginRoute: typeof authLoginRoute
authUserResetpasswordRoute: typeof authUserResetpasswordRoute
authUserSignupRoute: typeof authUserSignupRoute
}
declare module '@tanstack/react-router' {
@@ -140,6 +171,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminLayoutAdminServersRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/(auth)/user/signup': {
id: '/(auth)/user/signup'
path: '/user/signup'
fullPath: '/user/signup'
preLoaderRoute: typeof authUserSignupRouteImport
parentRoute: typeof rootRouteImport
}
'/(auth)/user/resetpassword': {
id: '/(auth)/user/resetpassword'
path: '/user/resetpassword'
fullPath: '/user/resetpassword'
preLoaderRoute: typeof authUserResetpasswordRouteImport
parentRoute: typeof rootRouteImport
}
}
}
@@ -162,6 +207,8 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
authLoginRoute: authLoginRoute,
authUserResetpasswordRoute: authUserResetpasswordRoute,
authUserSignupRoute: authUserSignupRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -46,6 +46,7 @@ export default function LoginForm() {
const session = await getSession();
setSession(session);
form.reset();
fetchRoles();
toast.success(
@@ -54,12 +55,13 @@ export default function LoginForm() {
router.invalidate();
router.history.push(search.redirect ? search.redirect : "/");
} catch (error) {
console.log(error);
// @ts-ignore
toast.error(error?.message);
}
},
});
return (
<div className="ml-[25%]">
<div className="">
<LstCard className="p-3 w-96">
<CardHeader>
<CardTitle>Login to your account</CardTitle>
@@ -87,9 +89,8 @@ export default function LoginForm() {
<form.AppField
name="password"
children={(field) => (
<field.InputField
<field.InputPasswordField
label="Password"
inputType="password"
required={true}
/>
)}
@@ -102,7 +103,7 @@ export default function LoginForm() {
)}
/>
<Link
to="/"
to="/user/resetpassword"
className="inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
@@ -118,9 +119,12 @@ export default function LoginForm() {
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<a href="#" className="underline underline-offset-4">
<Link
to={"/user/signup"}
className="underline underline-offset-4"
>
Sign up
</a>
</Link>
</div>
</CardContent>
</LstCard>

View File

@@ -0,0 +1,88 @@
import { LstCard } from "../../../components/ui/lstCard";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { useAppForm } from "../../../lib/formStuff";
import { api } from "../../../lib/axiosAPI";
import { toast } from "sonner";
import { Link } from "@tanstack/react-router";
export default function RequestResetPassword() {
const form = useAppForm({
defaultValues: {
email: "",
},
onSubmit: async ({ value }) => {
try {
const res = await api.post("api/user/resetpassword", {
email: value.email,
});
console.log(res);
if (res.status === 200) {
toast.success(
res.data.message
? res.data.message
: "If this email exists in our system, check your email for the reset link"
);
}
} catch (error) {
console.log(error);
}
},
});
return (
<div className="">
<LstCard className="p-6 w-96">
<CardHeader>
<CardTitle>Reset your password</CardTitle>
<CardDescription>
Enter your email address and well send you a reset link
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="email"
children={(field) => (
<field.InputField
label="Email address"
inputType="email"
required={true}
/>
)}
/>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>
Send Reset Link
</form.SubmitButton>
</form.AppForm>
</div>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
Remembered your password?{" "}
<Link
to="/login"
className="text-primary underline underline-offset-4 hover:text-primary/80"
>
Back to login
</Link>
</div>
</CardContent>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import { useAppForm } from "../../../lib/formStuff";
import { LstCard } from "../../../components/ui/lstCard";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { api } from "../../../lib/axiosAPI";
import { toast } from "sonner";
import { Link, useNavigate } from "@tanstack/react-router";
export default function ResetPasswordForm({ token }: { token: string }) {
const navigate = useNavigate();
const form = useAppForm({
defaultValues: {
password: "",
confirmPassword: "",
},
onSubmit: async ({ value }) => {
if (value.password != value.confirmPassword) {
toast.error("Passwords do not match");
return;
}
try {
const res = await api.post("/api/auth/reset-password", {
newPassword: value.password,
token: token,
});
if (res.status === 200) {
toast.success("Password has been reset");
form.reset();
navigate({ to: "/login" });
}
} catch (error) {
console.log(error);
// @ts-ignore
toast.error(error?.response.data.message);
}
},
});
return (
<div className="">
<LstCard className="p-6 w-96">
<CardHeader>
<CardTitle>Set a new password</CardTitle>
<CardDescription>
Enter your new password below and confirm it to continue
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="password"
children={(field) => (
<field.InputPasswordField
label="New Password"
required={true}
/>
)}
/>
<form.AppField
name="confirmPassword"
// validators={{
// onChangeListenTo: ["password"],
// onChange: ({ value, fieldApi }) => {
// if (
// value !==
// fieldApi.form.getFieldValue("password")
// ) {
// return "Passwords do not match";
// }
// return undefined;
// },
// }}
children={(field) => (
<field.InputPasswordField
label="Confirm Password"
required={true}
/>
)}
/>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>
Reset Password
</form.SubmitButton>
</form.AppForm>
</div>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
Remembered your account?{" "}
<Link
to="/login"
className="text-primary underline underline-offset-4 hover:text-primary/80"
>
Back to login
</Link>
</div>
</CardContent>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,129 @@
import { toast } from "sonner";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { LstCard } from "../../../components/ui/lstCard";
import { api } from "../../../lib/axiosAPI";
import { useAppForm } from "../../../lib/formStuff";
import { Link } from "@tanstack/react-router";
export default function SignupForm() {
const form = useAppForm({
defaultValues: {
username: "",
email: "",
password: "",
confirmPassword: "",
},
onSubmit: async ({ value }) => {
if (value.password != value.confirmPassword) {
toast.error("Passwords do not match");
return;
}
try {
const res = await api.post("/api/user/register", {
username: value.username,
name: value.username,
email: value.email,
password: value.password,
});
if (res.status === 200) {
toast.success(`Welcome ${value.username}, to lst.`);
}
} catch (error) {
console.log(error);
// @ts-ignore
toast.error(error?.response.data.message);
}
},
});
return (
<div className="">
<LstCard className="p-6 w-96">
<CardHeader>
<CardTitle>Create an account</CardTitle>
<CardDescription>
Fill in your details to get started
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
{/* Username */}
<form.AppField
name="username"
children={(field) => (
<field.InputField
label="Username"
inputType="text"
required={true}
/>
)}
/>
{/* Email */}
<form.AppField
name="email"
children={(field) => (
<field.InputField
label="Email address"
inputType="email"
required={true}
/>
)}
/>
{/* Password */}
<form.AppField
name="password"
children={(field) => (
<field.InputField
label="Password"
inputType="password"
required={true}
/>
)}
/>
{/* Confirm Password */}
<form.AppField
name="confirmPassword"
children={(field) => (
<field.InputField
label="Confirm Password"
inputType="password"
required={true}
/>
)}
/>
<div className="flex justify-end mt-6">
<form.AppForm>
<form.SubmitButton>Sign Up</form.SubmitButton>
</form.AppForm>
</div>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link
to={"/login"}
className="text-primary underline underline-offset-4 hover:text-primary/80"
>
Log in
</Link>
</div>
</CardContent>
</LstCard>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export const Route = createFileRoute("/(auth)/login")({
function RouteComponent() {
return (
<div className="m-[25%]">
<div className="ml-[25%] mt-[0.5%]">
<LoginForm />
</div>
);

View File

@@ -0,0 +1,27 @@
import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import RequestResetPassword from "../-components/RequestResetPassword";
import ResetPasswordForm from "../-components/ResetPasswordForm";
export const Route = createFileRoute("/(auth)/user/resetpassword")({
// beforeLoad: ({ search }) => {
// return { token: search.token };
// },
validateSearch: z.object({
token: z.string().optional(),
}),
component: RouteComponent,
});
function RouteComponent() {
const { token } = Route.useSearch();
return (
<div className="ml-[25%] mt-[0.5%]">
{token ? (
<ResetPasswordForm token={token} />
) : (
<RequestResetPassword />
)}
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import SignupForm from "../-components/SignupForm";
export const Route = createFileRoute("/(auth)/user/signup")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div className="ml-[25%] mt-[0.5%]">
<SignupForm />
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import type { QueryClient } from "@tanstack/react-query";
import { Toaster } from "sonner";
import Cookies from "js-cookie";
@@ -18,7 +18,6 @@ interface RootRouteContext {
const RootLayout = () => {
//const { logout, login } = Route.useRouteContext();
const defaultOpen = Cookies.get("sidebar_state") === "true";
return (
<div>
@@ -26,16 +25,15 @@ const RootLayout = () => {
<ThemeProvider>
<div className="flex flex-col h-screen overflow-hidden">
<Nav />
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 overflow-hidden">
<SidebarProvider defaultOpen={defaultOpen}>
<SideBarNav />
<div className="flex-1 overflow-y-auto">
<div className="flex-2 overflow-y-auto">
<Outlet />
</div>
</SidebarProvider>
</div>
<Toaster expand richColors closeButton />
<TanStackRouterDevtools />
</div>
</ThemeProvider>
</SessionGuard>

View File

@@ -1,25 +1,13 @@
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "../components/ui/button";
import { useAuth } from "../lib/authClient";
export const Route = createFileRoute("/")({
component: Index,
});
function Index() {
const { session } = useAuth();
return (
<div>
<div className="h-screen flex flex-col items-center justify-center">
<div>Welcome, {session ? session.user.username : "Guest"}</div>
<div>
<Button className="h-96 w-96">
<a href="/lst/d" target="_blank" className="text-4xl">
LST-DOCS
</a>
</Button>
</div>
</div>
<div className="h-screen flex flex-col items-center justify-center"></div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { useState } from "react";
import { Label } from "@/components/ui/label";
import { useFieldContext } from "..";
import { Input } from "@/components/ui/input";
import { FieldErrors } from "./FieldErrors";
import { Button } from "@/components/ui/button";
import { Eye, EyeOff } from "lucide-react";
type PasswordInputProps = {
label: string;
required?: boolean;
};
export const InputPassword = ({
label,
required = false,
}: PasswordInputProps) => {
const field = useFieldContext<any>();
const [show, setShow] = useState(false);
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<div className="relative">
<Input
id={field.name}
type={show ? "text" : "password"}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
required={required}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShow((p) => !p)}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7"
tabIndex={-1} // prevent messing with tab order
>
{show ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -1,11 +1,14 @@
import axios from "axios";
import querystring from "querystring";
import { createLog } from "../../../../logger/logger.js";
import { saveXml } from "../../../utils/saveXml.js";
//tiCreds
const userid = process.env.USERID || "";
const password = process.env.PASSWORD || "";
export const postToTi = async (data: string) => {
export const postToTi = async (data: string, fileName: string) => {
// 🔹 Save the XML before posting
saveXml(data, fileName);
const formBody = querystring.stringify({
userid,
password,

View File

@@ -272,7 +272,14 @@ export const tiImport = async () => {
//console.log("payload", payload);
const { data: tiPost, error: tiError } = await tryCatch(postToTi(payload));
const { data: tiPost, error: tiError } = await tryCatch(
postToTi(
payload,
`${orderData[0].addressAlias.replaceAll(" ", "")}-${
header[0].releaseNumber
}`
)
);
if (tiError) {
return {

View File

@@ -0,0 +1,47 @@
import fs from "fs";
import path from "path";
import { createLog } from "../../logger/logger.js";
/**
* Save XML string to /xml/ with timestamp in name
* @param xmlContent The XML string
* @param prefix File prefix (ex: request or response)
*/
export const saveXml = (xmlContent: string, prefix: string = "request") => {
try {
const now = new Date();
// Format YYYYMMDDHHmm
const timestamp = `${now.getFullYear()}${String(
now.getMonth() + 1
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(
now.getHours()
).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}`;
// Ensure xml folder path is always relative to project root
const xmlDir = path.resolve(process.cwd(), "xml");
const filePath = path.join(xmlDir, `${prefix}_${timestamp}.xml`);
// Make sure xml folder exists
if (!fs.existsSync(xmlDir)) {
fs.mkdirSync(xmlDir, { recursive: true });
}
// Save file
fs.writeFileSync(filePath, xmlContent, "utf-8");
createLog(
"info",
"notification",
"notification",
`XML saved to: ${filePath}`
);
} catch (error) {
createLog(
"error",
"notification",
"notification",
`Error saving XML:", ${JSON.stringify(error)}`
);
}
};

View File

@@ -1,11 +1,6 @@
import { ControllerManager } from "st-ethernet-ip";
import { getMac } from "./getMachineId.js";
import { format } from "date-fns-tz";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { getCurrentLabel } from "../../sqlServer/querys/ocp/getLabel.js";
import { query } from "../../sqlServer/prodSqlServer.js";
import { createLog } from "../../logger/logger.js";
import { getMac } from "../../../utils/getMachineId.js";
import { labelingProcess } from "../../labeling/labelProcess.js";
export const createPlcMonitor = (config: any) => {
let cm: any;
@@ -77,34 +72,39 @@ export const createPlcMonitor = (config: any) => {
setTimeout(async () => {
if (tag.value === 0) return;
const macId = await getMac(tag.value);
console.log(macId);
const { data, error } = (await tryCatch(
query(
getCurrentLabel
.replace(
"[macId]",
macId[0]?.HumanReadableId
)
.replace(
"[time]",
format(time, "yyyy-MM-dd HH:mm")
),
"Current label data"
)
)) as any;
// const { data, error } = (await tryCatch(
// query(
// getCurrentLabel
// .replace(
// "[macId]",
// macId[0]?.HumanReadableId
// )
// .replace(
// "[time]",
// format(time, "yyyy-MM-dd HH:mm")
// ),
// "Current label data"
// )
// )) as any;
createLog(
"info",
"zechettii",
"zechettii",
`${format(time, "yyyy-MM-dd HH:mm")} [${cfg.id}] ${
tag.name
}: ${prevVal} -> ${
tag.value
}, the running number is ${
error ? null : data.data[0]?.LfdNr
}}`
);
// createLog(
// "info",
// "zechettii",
// "zechettii",
// `${format(time, "yyyy-MM-dd HH:mm")} [${cfg.id}] ${
// tag.name
// }: ${prevVal} -> ${
// tag.value
// }, the running number is ${
// error ? null : data.data[0]?.LfdNr
// }}`
// );
const zechette = {
line: tag.value.toString(),
printer: cfg.printerId, // this is the id of the zechetti 2 to print we should move this to the db
printerName: cfg.id,
};
labelingProcess({ zechette: zechette });
}, 1000);
}
});

View File

@@ -1,20 +1,22 @@
import { createPlcMonitor } from "../../../utils/plcController.js";
import { createPlcMonitor } from "./plcController.js";
export const zechettiConnect = () => {
const config: any = {
controllers: [
{
id: "Z1",
id: "Zecchetti_1",
ip: "192.168.193.97",
slot: 0,
rpi: 250,
printerId: 22, // grabbed from 2.0
tags: ["N7[0]"],
},
{
id: "Z2",
id: "Zecchetti_2",
ip: "192.168.193.111",
slot: 0,
rpi: 100,
printerId: 23,
tags: ["N8[0]"],
},
],

View File

@@ -25,7 +25,7 @@ import labelRatio from "./routes/labeling/getLabelRatio.js";
import resetRatio from "./routes/labeling/resetLabelRatio.js";
import materialTransferLot from "./routes/materials/lotTransfer.js";
import pendingTransfers from "./routes/materials/currentPending.js";
import { createPlcMonitor } from "./utils/plcController.js";
import { createPlcMonitor } from "./controller/specialProcesses/zechettis/plcController.js";
import { zechettiConnect } from "./controller/specialProcesses/zechettis/zechettiConnect.js";
const app = new OpenAPIHono();

View File

@@ -55,13 +55,32 @@ MaterialHumanReadableId
,case when cp.Pieces >= 0.001 then (lot.TotalProducedLoadingUnits+1) * cp.Pieces else
(a.Weight *((case when cp.Percentage is null then 80 else cp.Percentage end) / 100) * ((lot.TotalProducedLoadingUnits+1) * p.LoadingUnitPieces)) / 1000 end totalNeeded
,case when IsMainMaterial = 1 then
case when (case when x.ProvidedAmount <> 0
then x.ProvidedAmount else x.EffectiveConsumption end) >
(case when cp.Pieces >= 0.001 then (lot.TotalProducedQuantity+1) * cp.Pieces else
(a.Weight *((case when cp.Percentage is null then 80 else cp.Percentage end) / 100) * ((lot.TotalProducedLoadingUnits+1) * p.LoadingUnitPieces)) / 1000 end)
then 'mmGood'
else 'noMM' end else null end as noMMShortage
,CASE
WHEN IsMainMaterial = 1 THEN
CASE
WHEN
(CASE
WHEN x.ProvidedAmount > 0
THEN x.ProvidedAmount
ELSE x.EffectiveConsumption
END)
>
(CASE
WHEN cp.Pieces >= 0.001
THEN (lot.TotalProducedQuantity + 1) * cp.Pieces
ELSE
(a.Weight *
(CASE
WHEN cp.Percentage IS NULL THEN 80
ELSE cp.Percentage
END) / 100
) * (lot.TotalProducedQuantity + 1) / 1000
END)
THEN 'mmGood'
ELSE 'noMM'
END
ELSE NULL
END AS noMMShortage
-- pkg check auto
,case when pkg.QuantityPosition is null then null else

View File

@@ -154,6 +154,14 @@ app.Run(async (HttpContext context) =>
}
}
//Inject client IP into X-Forwarded-For
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
if (!string.IsNullOrEmpty(remoteIp))
{
request.Headers.Remove("X-Forwarded-For");
request.Headers.TryAddWithoutValidation("X-Forwarded-For", remoteIp);
}
if (context.Request.ContentLength > 0 && request.Content == null)
{
request.Content = new StreamContent(context.Request.Body);

View File

@@ -0,0 +1,10 @@
CREATE TABLE "apiHits" (
"apiHit_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"method" text NOT NULL,
"path" text NOT NULL,
"body" jsonb,
"status" integer,
"ip" text,
"duration" integer,
"createdAt" timestamp DEFAULT now() NOT NULL
);

View File

@@ -0,0 +1,887 @@
{
"id": "33b56ace-aa18-498f-922f-513e13ccc6ef",
"prevId": "2e845c85-5248-4082-a69a-77c18e50f044",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.apiHits": {
"name": "apiHits",
"schema": "",
"columns": {
"apiHit_id": {
"name": "apiHit_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"method": {
"name": "method",
"type": "text",
"primaryKey": false,
"notNull": true
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"ip": {
"name": "ip",
"type": "text",
"primaryKey": false,
"notNull": false
},
"duration": {
"name": "duration",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.apikey": {
"name": "apikey",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start": {
"name": "start",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refill_interval": {
"name": "refill_interval",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"refill_amount": {
"name": "refill_amount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_refill_at": {
"name": "last_refill_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_enabled": {
"name": "rate_limit_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_time_window": {
"name": "rate_limit_time_window",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 86400000
},
"rate_limit_max": {
"name": "rate_limit_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 10
},
"request_count": {
"name": "request_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"remaining": {
"name": "remaining",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_request": {
"name": "last_request",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"permissions": {
"name": "permissions",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"apikey_user_id_user_id_fk": {
"name": "apikey_user_id_user_id_fk",
"tableFrom": "apikey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.jwks": {
"name": "jwks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"last_login": {
"name": "last_login",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"log_id": {
"name": "log_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": false
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"subModule": {
"name": "subModule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stack": {
"name": "stack",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"settings_id": {
"name": "settings_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"moduleName": {
"name": "moduleName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"roles": {
"name": "roles",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[\"systemAdmin\"]'::jsonb"
},
"add_User": {
"name": "add_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"add_Date": {
"name": "add_Date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"upd_User": {
"name": "upd_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"upd_date": {
"name": "upd_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"name": {
"name": "name",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_roles": {
"name": "user_roles",
"schema": "",
"columns": {
"user_role_id": {
"name": "user_role_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"unique_user_module": {
"name": "unique_user_module",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "module",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_roles_user_id_user_id_fk": {
"name": "user_roles_user_id_user_id_fk",
"tableFrom": "user_roles",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -50,6 +50,13 @@
"when": 1758588364052,
"tag": "0006_loud_reavers",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1758755206107,
"tag": "0007_common_kronos",
"breakpoints": true
}
]
}

1877
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,7 @@
"type": "module",
"dependencies": {
"@dotenvx/dotenvx": "^1.49.0",
"@tanstack/react-table": "^8.21.3",
"@types/cors": "^2.8.19",
"better-auth": "^1.3.9",
"cors": "^2.8.5",
@@ -49,8 +50,11 @@
"drizzle-orm": "^0.44.5",
"drizzle-zod": "^0.8.3",
"express": "^5.1.0",
"handlebars": "^4.7.8",
"morgan": "^1.10.1",
"mssql": "^11.0.1",
"nodemailer": "^7.0.6",
"nodemailer-express-handlebars": "^7.0.0",
"pg": "^8.16.3",
"pino": "^9.9.0",
"pino-pretty": "^13.1.1",
@@ -62,6 +66,8 @@
"@types/morgan": "^1.9.10",
"@types/mssql": "^9.1.7",
"@types/node": "^24.3.0",
"@types/nodemailer": "^7.0.1",
"@types/nodemailer-express-handlebars": "^4.0.5",
"concurrently": "^9.2.1",
"cz-conventional-changelog": "^3.3.0",
"standard-version": "^9.5.0",

View File

@@ -14,7 +14,7 @@ param (
# example string to pass over, you must be in the script dir when you run this script. or it will fail to find the linked scripts
# If we do not pass plant to update over it will auto do all plants if we want a specific plant we need to do like below
# .\update-controllers.ps1 -App_Path "C:\Users\matthes01\Documents\lst" -Token "usiow2" -BuildController yes -PlantToUpdate "usiow1vms006" -Remote_Path "D$\LST\lst_2"
# .\update-controllers.ps1 -App_Path "C:\Users\matthes01\Documents\lst" -Token "test3" -BuildController yes -PlantToUpdate "usmcd1vms036" -Remote_Path "E$\LST"
# .\update-controllers.ps1 -App_Path "C:\Users\matthes01\Documents\lst" -Token "test3" -BuildController yes
$Plants = @(
@@ -27,6 +27,7 @@ $Plants = @(
@{ Server = "usflo1vms006"; Token = "usflo1" ; Remote_Path = "E$\LST" }
@{ Server = "usiow1vms006"; Token = "usiow1" ; Remote_Path = "D$\LST\lst" }
@{ Server = "usiow1vms006"; Token = "usiow2" ; Remote_Path = "D$\LST\lst_2" }
@{ Server = "uslim1vms006"; Token = "uslim1" ; Remote_Path = "E$\LST" }
@{ Server = "usmcd1vms006"; Token = "usmcd1" ; Remote_Path = "E$\LST" }
@{ Server = "usslc1vms006"; Token = "usslc1" ; Remote_Path = "E$\LST" }
@{ Server = "usstp1vms006"; Token = "usstp1" ; Remote_Path = "D$\LST" }
@@ -107,6 +108,8 @@ if (-Not (Test-Path $BuildFolder)) {
# only rebuild if we push yes over
if ($BuildController -eq "yes") {
# now we can create the zip
Zip-Includes -IncludesFile $ControlIncludes -BuildNumber $BuildNumber -BuildFolder $BuildFolder -AppRoot $App_Path
Start-Sleep -Seconds 3