feat(intial auth): intial auth setup for the scanner
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled

This commit is contained in:
2026-05-05 19:48:36 -05:00
parent 4e0cf8c54c
commit cd13360cfb
46 changed files with 7847 additions and 206 deletions

View File

@@ -1,6 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres"; import postgres from "postgres";
import * as scanUserSchema from "./schema/scanUsers.js";
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`; const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
const queryClient = postgres(dbURL, { const queryClient = postgres(dbURL, {
@@ -13,4 +15,10 @@ const queryClient = postgres(dbURL, {
}, },
}); });
export const db = drizzle({ client: queryClient }); //export const db = drizzle({ client: queryClient });
export const db = drizzle(queryClient, {
schema: {
...scanUserSchema,
},
});

View File

@@ -0,0 +1,47 @@
import {
boolean,
pgEnum,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type z from "zod";
export const mobileRoleEnum = pgEnum("mobile_role", [
"user",
"lead",
"manager",
"admin",
]);
export const scanUser = pgTable(
"scan_users",
{
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(), // the user that will be using the scanner
scannerId: text("scanner_id").unique().notNull(),
pinNumber: text("pin_number").unique().notNull(),
pinHash: text("pin_hash").notNull(),
excludedCommand: text("excluded_commands").default(""),
role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(),
add_Date: timestamp("add_Date").defaultNow(),
upd_date: timestamp("upd_date").defaultNow(),
},
(table) => ({
userNotificationUnique: unique("scan_user_unique").on(
table.scannerId,
table.pinNumber,
),
}),
);
export const scanUserSchema = createSelectSchema(scanUser);
export const newsSanUserSchema = createInsertSchema(scanUser);
export type ScanUser = z.infer<typeof scanUserSchema>;
export type NewScanUser = z.infer<typeof newsSanUserSchema>;

View File

@@ -0,0 +1,46 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
import { fileURLToPath } from "url";
const router = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const downloadDir = path.resolve(__dirname, "../../downloads/mobile");
const currentApk = {
fileName: "lst-mobile.apk",
};
router.get("/latest", (_, res) => {
const apkPath = path.join(downloadDir, currentApk.fileName);
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader(
"Content-Disposition",
`attachment; filename="${currentApk.fileName}"`,
);
return res.sendFile(apkPath);
});
router.get("/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
export default router;

View File

@@ -0,0 +1,17 @@
import type { Express } from "express";
import downloads from "./donwloadApps.route.js";
import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js";
import logs from "./scanLogs.route.js";
import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads);
app.use(`${baseUrl}/api/mobile/logs`, logs);
app.use(`${baseUrl}/api/mobile/auth`, authPin);
app.use(`${baseUrl}/api/mobile/pin`, newPin);
// all other system should be under /api/system/*
};

View File

@@ -0,0 +1,331 @@
import bcrypt from "bcryptjs";
import { eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import {
type NewScanUser,
type ScanUser,
scanUser,
} from "../db/schema/scanUsers.js";
import { requireAuth } from "../middleware/auth.middleware.js";
import { apiReturn, returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
export async function hashPin(pin: string) {
// if (!/^\d{6}$/.test(pin)) {
// throw new Error("PIN must be exactly 6 digits");
// }
return bcrypt.hashSync(pin, 12);
}
const registerSchema = z.object({
name: z.string().min(2).max(100),
pinNumber: z.string(),
scannerId: z
.string()
.min(1)
.max(500)
.optional()
.describe("if you leave blank it will be the same as your username"),
role: z
.enum(["user", "lead", "manager", "admin"])
.optional()
.describe("What roles are available to use."),
pinHash: z.string().optional(),
});
r.post("/pin", async (req, res) => {
const { pin } = req.body;
if (!pin || pin.length !== 6) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Pin number must be a min of 6 digits`,
data: [],
status: 401,
});
}
// const user = await db
// .select()
// .from(scanUser)
// .where(eq(scanUser.pinNumber, parseInt(pin, 10)));
const user = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinNumber, pin),
});
if (!user) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid login please try again.`,
data: [],
status: 401,
});
}
const validPin = bcrypt.compareSync(pin, user.pinHash);
if (!validPin) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid pin please try again.`,
data: [],
status: 401,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `Welcome back ${user.name}`,
data: user as ScanUser | any,
status: 200,
});
});
r.post("/user", async (req, res) => {
try {
// validate the body is correct before accepting it
let validated = registerSchema.parse(req.body);
validated = {
...validated,
pinHash: await hashPin(validated.pinNumber.toString()),
};
const values: NewScanUser = {
name: validated.name,
pinNumber: validated.pinNumber,
pinHash: validated.pinHash ?? "",
scannerId: validated.scannerId ?? "",
};
const newUser = await db.insert(scanUser).values(values).returning();
apiReturn(res, {
success: true,
level: "info", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: `${validated.name} was just created`,
data: newUser as any,
status: 200, //connect.success ? 200 : 400,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "mobile",
subModule: "auth",
message:
"This User already exist with this pin or scanner id please try again",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser));
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `There are no users you should add one . `,
data: [],
status: 200,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "auth",
message: `All users. `,
data,
status: 200,
});
});
r.patch("/user/:id", requireAuth, async (req, res) => {
const updates: Record<string, unknown | null> = {};
const { id } = req.params;
const { data, error } = await tryCatch(
db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.id, `${id}`),
}),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error getting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `Invalid user id was passed over. `,
data: [],
status: 400,
});
}
if (req.body?.name !== undefined) {
updates.name = req.body.name;
}
if (req.body?.pinNumber !== undefined) {
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, req.body.pinNumber),
});
if (existing)
return returnFunc({
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `${req.body.pinNumber} already exists please try again`,
data: [],
notify: false,
room: "",
});
updates.pinNumber = req.body.pinNumber;
updates.pinHash = await hashPin(req.body.pinNumber);
}
if (req.body?.scannerId !== undefined) {
updates.scannerId = req.body.scannerId;
}
if (req.body?.active !== undefined) {
updates.active = req.body.active;
}
if (req.body?.role !== undefined) {
updates.role = req.body.role;
}
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(scanUser)
.set(updates)
.where(eq(scanUser.id, `${id}`))
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User ${data.name} was updated. `,
data: updatedSetting,
status: 200,
});
});
r.delete("/user/:id", requireAuth, async (req, res) => {
const { id } = req.params;
const { data, error } = await tryCatch(
db.delete(scanUser).where(eq(scanUser.id, `${id}`)),
);
if (error) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was an error deleting the user`,
data: error as any,
status: 400,
});
}
if (!data) {
return apiReturn(res, {
success: false,
level: "error",
module: "mobile",
subModule: "auth",
message: `There was no user to delete. `,
data: [],
status: 400,
});
}
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "user",
message: `User was deleted. `,
data: data ?? [],
status: 200,
});
});
export default r;

View File

@@ -0,0 +1,21 @@
import { Router } from "express";
import { generateUniquePin } from "../utils/generateScannerPin.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/new", async (_, res) => {
const getPin = await generateUniquePin();
return apiReturn(res, {
success: getPin.success,
level: getPin.level,
module: "mobile",
subModule: "auth",
message: getPin.message,
data: getPin.data,
status: getPin.success ? 200 : 400,
});
});
export default r;

View File

@@ -0,0 +1,35 @@
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
router.post("/", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.scannerId,
message: body.message,
prompt: body.prompt,
commandDescription: body.commandDescription,
status: body.status,
lines: body.lines,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

@@ -0,0 +1,28 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
const router = Router();
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
export default router;

View File

@@ -5,6 +5,7 @@ import { setupAuthRoutes } from "./auth/auth.routes.js";
import { setupApiDocsRoutes } from "./configs/scaler.config.js"; import { setupApiDocsRoutes } from "./configs/scaler.config.js";
import { setupDatamartRoutes } from "./datamart/datamart.routes.js"; import { setupDatamartRoutes } from "./datamart/datamart.routes.js";
import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js"; import { setupGPSqlRoutes } from "./gpSql/gpSql.routes.js";
import { setupMobileRoutes } from "./mobile/mobile.routes.js";
import { setupNotificationRoutes } from "./notification/notification.routes.js"; import { setupNotificationRoutes } from "./notification/notification.routes.js";
import { setupOCPRoutes } from "./ocp/ocp.routes.js"; import { setupOCPRoutes } from "./ocp/ocp.routes.js";
import { setupOpendockRoutes } from "./opendock/opendock.routes.js"; import { setupOpendockRoutes } from "./opendock/opendock.routes.js";
@@ -27,4 +28,5 @@ export const setupRoutes = (baseUrl: string, app: Express) => {
setupNotificationRoutes(baseUrl, app); setupNotificationRoutes(baseUrl, app);
setupOCPRoutes(baseUrl, app); setupOCPRoutes(baseUrl, app);
setupTCPRoutes(baseUrl, app); setupTCPRoutes(baseUrl, app);
setupMobileRoutes(baseUrl, app);
}; };

View File

@@ -34,7 +34,7 @@ const servers: NewServerData[] = [
name: "Lima", name: "Lima",
server: "USLIM1VMS006", server: "USLIM1VMS006",
plantToken: "uslim1", plantToken: "uslim1",
idAddress: "10.53.0.26", idAddress: "10.53.0.26", // port opened 3000 2222
greatPlainsPlantCode: "50", greatPlainsPlantCode: "50",
contactEmail: "", contactEmail: "",
contactPhone: "", contactPhone: "",
@@ -56,7 +56,7 @@ const servers: NewServerData[] = [
name: "Dayton", name: "Dayton",
server: "usday1VMS006", server: "usday1VMS006",
plantToken: "usday1", plantToken: "usday1",
idAddress: "10.44.0.56", // 3000 opened and working idAddress: "10.44.0.56", // ports opened 3000 and 2222
greatPlainsPlantCode: "80", greatPlainsPlantCode: "80",
contactEmail: "", contactEmail: "",
contactPhone: "", contactPhone: "",
@@ -122,7 +122,7 @@ const servers: NewServerData[] = [
name: "Marked Tree", name: "Marked Tree",
server: "USMAR1VMS006", server: "USMAR1VMS006",
plantToken: "usmar1", plantToken: "usmar1",
idAddress: "10.206.9.26", idAddress: "10.206.9.26", // 3000,2222 requested REQ0236838
greatPlainsPlantCode: "90", greatPlainsPlantCode: "90",
contactEmail: "", contactEmail: "",
contactPhone: "", contactPhone: "",
@@ -144,7 +144,7 @@ const servers: NewServerData[] = [
name: "Bowling Green 1", name: "Bowling Green 1",
server: "USBOW1VMS006", server: "USBOW1VMS006",
plantToken: "usbow1", plantToken: "usbow1",
idAddress: "10.25.0.26", // 3000 is open REQ0236527 idAddress: "10.25.0.26", // 3000 is open REQ0236527 2222 already open
greatPlainsPlantCode: "55", greatPlainsPlantCode: "55",
contactEmail: "", contactEmail: "",
contactPhone: "", contactPhone: "",

View File

@@ -1,93 +0,0 @@
import fs from "node:fs";
import { Router } from "express";
import path from "path";
import { fileURLToPath } from "url";
import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const downloadDir = path.resolve(__dirname, "../../downloads/mobile");
const projectRoot = path.resolve("./lstMobile"); // adjust as needed
const appJsonPath = path.join(projectRoot, "app.json");
const currentApk = {
fileName: "lst-mobile.apk",
};
router.get("/version", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`;
const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw);
const exp = config.expo;
res.json({
packageName: exp.android?.package,
versionName: exp.version,
versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
});
});
router.get("/apk/latest", (_, res) => {
const apkPath = path.join(downloadDir, currentApk.fileName);
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader(
"Content-Disposition",
`attachment; filename="${currentApk.fileName}"`,
);
return res.sendFile(apkPath);
});
router.get("/apk/ehs", (_, res) => {
const apkPath = path.join(downloadDir, "EHS.apk");
if (!fs.existsSync(apkPath)) {
return res.status(404).json({ success: false, message: "APK not found" });
}
res.setHeader("Content-Type", "application/vnd.android.package-archive");
res.setHeader("Content-Disposition", `attachment; filename="EHS.apk}"`);
return res.sendFile(apkPath);
});
router.post("/logs", async (req, res) => {
const body = req.body;
const newLog = await db
.insert(scanLog)
.values({
scannerId: body.data.scannerId,
message: body.data.message,
prompt: body.data.prompt,
commandDescription: body.data.commandDescription,
status: body.data.status,
lines: body.data.lines,
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scan logs",
message: `New log from ${body.data.scannerId}`,
data: newLog,
status: 200,
});
});
export default router;

View File

@@ -4,12 +4,10 @@ import getServers from "./serverData.route.js";
import getSettings from "./settings.route.js"; import getSettings from "./settings.route.js";
import updSetting from "./settingsUpdate.route.js"; import updSetting from "./settingsUpdate.route.js";
import stats from "./stats.route.js"; import stats from "./stats.route.js";
import mobile from "./system.mobileApp.js";
export const setupSystemRoutes = (baseUrl: string, app: Express) => { export const setupSystemRoutes = (baseUrl: string, app: Express) => {
//stats will be like this as we dont need to change this //stats will be like this as we dont need to change this
app.use(`${baseUrl}/api/stats`, stats); app.use(`${baseUrl}/api/stats`, stats);
app.use(`${baseUrl}/api/mobile`, mobile);
app.use(`${baseUrl}/api/settings`, getSettings); app.use(`${baseUrl}/api/settings`, getSettings);
app.use(`${baseUrl}/api/servers`, getServers); app.use(`${baseUrl}/api/servers`, getServers);
app.use(`${baseUrl}/api/settings`, requireAuth, updSetting); app.use(`${baseUrl}/api/settings`, requireAuth, updSetting);

View File

@@ -0,0 +1,39 @@
import { db } from "../db/db.controller.js";
import { returnFunc } from "./returnHelper.utils.js";
export function generateSixDigitPin() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function generateUniquePin() {
for (let i = 0; i < 10; i++) {
const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below
});
if (!existing)
return returnFunc({
success: true,
level: "info",
module: "utils",
subModule: "genPin",
message: "New pin generated",
data: [{ pin: pin }],
notify: false,
room: "",
});
}
return returnFunc({
success: false,
level: "error",
module: "utils",
subModule: "genPin",
message: "Failed to generate unique PIN after 10 attempts",
data: [],
notify: true,
room: "",
});
}

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Bell, Logs, Server, Settings } from "lucide-react"; import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
import { import {
SidebarGroup, SidebarGroup,
@@ -56,22 +56,22 @@ export default function AdminSidebar({ session }: any) {
module: "admin", module: "admin",
active: true, active: true,
}, },
// { {
// title: "Modules", title: "Users",
// url: "/admin/modules", url: "/admin/users",
// icon: Settings, icon: UsersRound,
// role: ["systemAdmin", "admin"], role: ["systemAdmin", "admin"],
// module: "admin", module: "admin",
// active: true, active: true,
// }, },
// { {
// title: "Servers", title: "Scan users",
// url: "/admin/servers", url: "/admin/scanUsers",
// icon: Server, icon: UsersRound,
// role: ["systemAdmin", "admin"], role: ["systemAdmin", "admin"],
// module: "admin", module: "admin",
// active: true, active: true,
// }, },
]; ];
return ( return (
<SidebarGroup> <SidebarGroup>

View File

@@ -36,6 +36,17 @@ const docs = [
}, },
], ],
}, },
{
title: "Mobile",
url: "/updateInstructions",
isActive: false,
items: [
{
title: "Settings",
url: "/mobile-settings",
},
],
},
]; ];
export default function DocBar() { export default function DocBar() {
const { setOpen } = useSidebar(); const { setOpen } = useSidebar();

View File

@@ -0,0 +1,49 @@
import { Link } from "@tanstack/react-router";
import { ScanText, ScrollText } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
export default function MobileBar({ session }: any) {
const { setOpen } = useSidebar();
const items = [
{
title: "Update Instructions",
url: "/",
icon: ScrollText,
},
{
title: "Scan Log",
url: "/",
icon: ScanText,
},
];
console.log(session);
return (
<SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -8,6 +8,7 @@ import {
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import AdminSidebar from "./AdminBar"; import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar"; import DocBar from "./DocBar";
import MobileBar from "./MobileBar";
export function AppSidebar() { export function AppSidebar() {
const { data: session } = useSession(); const { data: session } = useSession();
@@ -22,7 +23,8 @@ export function AppSidebar() {
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarContent> <SidebarContent>
<DocBar/> <DocBar />
<MobileBar session={session} />
{session && {session &&
(session.user.role === "admin" || (session.user.role === "admin" ||
session.user.role === "systemAdmin") && ( session.user.role === "systemAdmin") && (

View File

@@ -0,0 +1,3 @@
export default function updateInstructions() {
return <div>updateInstructions</div>;
}

View File

@@ -11,6 +11,15 @@ import {
import React, { useState } from "react"; import React, { useState } from "react";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { import {
Table, Table,
TableBody, TableBody,
@@ -26,15 +35,23 @@ type LstTableType = {
tableClassName?: string; tableClassName?: string;
data: any; data: any;
columns: any; columns: any;
height?: string;
pageSize?: number;
}; };
export default function LstTable({ export default function LstTable({
className = "", className = "",
tableClassName = "", tableClassName = "",
data, data,
columns, columns,
height = "h-full",
pageSize = 5,
}: LstTableType) { }: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState({
pageIndex: 0, //initial page index
pageSize: pageSize, //default page size
});
//console.log(data); //console.log(data);
const table = useReactTable({ const table = useReactTable({
@@ -46,24 +63,33 @@ export default function LstTable({
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onPaginationChange: setPagination,
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />, //renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true, //getRowCanExpand: () => true,
// columnResizeMode: "onChange",
filterFns: {}, filterFns: {},
state: { state: {
sorting, sorting,
pagination,
columnFilters, columnFilters,
}, },
}); });
return ( return (
<div className={className}> <div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap"> <div>{/* TODO: Add table header in here like title */}</div>
<ScrollArea
className={`w-full rounded-md border whitespace-nowrap ${height}`}
>
<Table className={cn("w-full", tableClassName)}> <Table className={cn("w-full", tableClassName)}>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead
key={header.id}
className="sticky top-0 z-20 bg-background"
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
@@ -76,6 +102,7 @@ export default function LstTable({
</TableRow> </TableRow>
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows.length ? ( {table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
@@ -107,14 +134,23 @@ export default function LstTable({
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" /> <ScrollBar orientation="vertical" />
</ScrollArea> </ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4"> <div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
Previous {"<"}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -122,8 +158,42 @@ export default function LstTable({
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
Next {">"}
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
{">>"}
</Button>
<Select
value={pagination.pageSize.toString()}
onValueChange={(e) =>
setPagination({
...pagination,
pageSize: e === "all" ? data.length : parseInt(e, 10),
})
}
>
<SelectTrigger className="w-16">
<SelectValue
//id={field.name}
placeholder="Select Page"
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Page Size</SelectLabel>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="all">All</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div> </div>
</div> </div>
); );

View File

@@ -15,6 +15,7 @@ import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsSplatRouteImport } from './routes/docs/$' import { Route as DocsSplatRouteImport } from './routes/docs/$'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings' import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminServersRouteImport } from './routes/admin/servers' import { Route as AdminServersRouteImport } from './routes/admin/servers'
import { Route as AdminScanUsersRouteImport } from './routes/admin/scanUsers'
import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications' import { Route as AdminNotificationsRouteImport } from './routes/admin/notifications'
import { Route as AdminLogsRouteImport } from './routes/admin/logs' import { Route as AdminLogsRouteImport } from './routes/admin/logs'
import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authLoginRouteImport } from './routes/(auth)/login'
@@ -52,6 +53,11 @@ const AdminServersRoute = AdminServersRouteImport.update({
path: '/admin/servers', path: '/admin/servers',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminScanUsersRoute = AdminScanUsersRouteImport.update({
id: '/admin/scanUsers',
path: '/admin/scanUsers',
getParentRoute: () => rootRouteImport,
} as any)
const AdminNotificationsRoute = AdminNotificationsRouteImport.update({ const AdminNotificationsRoute = AdminNotificationsRouteImport.update({
id: '/admin/notifications', id: '/admin/notifications',
path: '/admin/notifications', path: '/admin/notifications',
@@ -89,6 +95,7 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginRoute '/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
@@ -103,6 +110,7 @@ export interface FileRoutesByTo {
'/login': typeof authLoginRoute '/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
@@ -118,6 +126,7 @@ export interface FileRoutesById {
'/(auth)/login': typeof authLoginRoute '/(auth)/login': typeof authLoginRoute
'/admin/logs': typeof AdminLogsRoute '/admin/logs': typeof AdminLogsRoute
'/admin/notifications': typeof AdminNotificationsRoute '/admin/notifications': typeof AdminNotificationsRoute
'/admin/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
@@ -134,6 +143,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
@@ -148,6 +158,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
@@ -162,6 +173,7 @@ export interface FileRouteTypes {
| '/(auth)/login' | '/(auth)/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/docs/$' | '/docs/$'
@@ -177,6 +189,7 @@ export interface RootRouteChildren {
authLoginRoute: typeof authLoginRoute authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute AdminSettingsRoute: typeof AdminSettingsRoute
DocsSplatRoute: typeof DocsSplatRoute DocsSplatRoute: typeof DocsSplatRoute
@@ -230,6 +243,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminServersRouteImport preLoaderRoute: typeof AdminServersRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/scanUsers': {
id: '/admin/scanUsers'
path: '/admin/scanUsers'
fullPath: '/admin/scanUsers'
preLoaderRoute: typeof AdminScanUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/notifications': { '/admin/notifications': {
id: '/admin/notifications' id: '/admin/notifications'
path: '/admin/notifications' path: '/admin/notifications'
@@ -281,6 +301,7 @@ const rootRouteChildren: RootRouteChildren = {
authLoginRoute: authLoginRoute, authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute, AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute, AdminNotificationsRoute: AdminNotificationsRoute,
AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute, AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute, AdminSettingsRoute: AdminSettingsRoute,
DocsSplatRoute: DocsSplatRoute, DocsSplatRoute: DocsSplatRoute,

View File

@@ -3,30 +3,36 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import Header from "@/components/Header"; import Header from "@/components/Header";
import { AppSidebar } from "@/components/Sidebar/sidebar"; import { AppSidebar } from "@/components/Sidebar/sidebar";
import { SidebarProvider } from "@/components/ui/sidebar"; import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/lib/theme-provider"; import { ThemeProvider } from "@/lib/theme-provider";
import { useSession } from "../lib/auth-client";
const RootLayout = () => ( const RootLayout = () => {
<div className="[--header-height:calc(--spacing(14))]"> const { data: session } = useSession();
<ThemeProvider> return (
<SidebarProvider className="flex flex-col" defaultOpen={false}> <div className="[--header-height:calc(--spacing(14))]">
<Header /> <ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header />
<div className="relative min-h-[calc(100svh-var(--header-height))]"> <div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar /> <AppSidebar />
<main className="w-full p-4"> <main className="w-full p-4">
<div className="mx-auto w-full max-w-7xl"> <div className="mx-auto w-full max-w-7xl">
<Outlet /> <Outlet />
</div> </div>
</main> </main>
</div> </div>
<Toaster expand richColors closeButton /> <Toaster expand richColors closeButton />
</SidebarProvider> </SidebarProvider>
</ThemeProvider> </ThemeProvider>
<TanStackRouterDevtools /> {session && session.user.role === "systemAdmin" && (
</div> <TanStackRouterDevtools />
); )}
</div>
);
};
export const Route = createRootRoute({ component: RootLayout }); export const Route = createRootRoute({ component: RootLayout });

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/admin/scanUsers')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/admin/scanUsers"!</div>
}

View File

@@ -155,7 +155,7 @@ const ServerTable = () => {
); );
} }
return <LstTable data={data} columns={columns} />; return <LstTable data={data} columns={columns} pageSize={50} />;
}; };
function RouteComponent() { function RouteComponent() {

View File

@@ -59,6 +59,33 @@ function RouteComponent() {
Only shows machines that are attached to the silo. Only shows machines that are attached to the silo.
</ul> </ul>
</ul> </ul>
{/* Mobile stuff */}
<li>Mobile App</li>
<ul className="list-disc list-inside indent-8">
<li>Rewrite of Alpla scan</li>
<ul className="list-disc list-inside indent-16">
<li>All old settings same as before id, ip, port</li>
<li>Currently scanned pallets will show now as well</li>
</ul>
<li>
Custom addition - login and more features NOTE: This is activated
based on how you enter the settings
</li>
<ul className="list-disc list-inside indent-16">
<li>Pin numbers login</li>
<li>
Scan a lane barcode and it returns whats in the lane and its
current status
</li>
<li>Command restrictions per pin login</li>
<li>Dock Door scanning</li>
<li>
More details on the pallet that is scanned by touching the running
number on the scanner.
</li>
</ul>
</ul>
{/* TMS integration */}
<li>TMS integration</li> <li>TMS integration</li>
<ul className="list-disc list-inside indent-8"> <ul className="list-disc list-inside indent-8">
<li>integration with TI to auto add in orders</li> <li>integration with TI to auto add in orders</li>

View File

@@ -15,7 +15,7 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 21, "versionCode": 23,
"minSupportedVersionCode": 21, "minSupportedVersionCode": 21,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile"
@@ -26,7 +26,7 @@
"bundler": "metro" "bundler": "metro"
}, },
"plugins": [ "plugins": [
"./plugins/withZebraScanner", "./plugins/withZebraDataWedge",
"expo-router", "expo-router",
[ [
"expo-splash-screen", "expo-splash-screen",

View File

@@ -9,7 +9,7 @@
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint", "lint": "expo lint",
"build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run copy:apk", "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat clean && gradlew.bat assembleRelease && npm run copy:apk",
"build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk", "build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease && npm run copy:apk",
"build:mobile": "cd scripts && node runBuild.ts", "build:mobile": "cd scripts && node runBuild.ts",
"build:mobile:bump": "cd scripts && node runBuild.ts --bump", "build:mobile:bump": "cd scripts && node runBuild.ts --bump",

View File

@@ -145,34 +145,32 @@ class ZebraScannerModule(
Thread.sleep(500) Thread.sleep(500)
val barcodeConfig = Bundle().apply { val barcodeConfig = Bundle().apply {
putString("PLUGIN_NAME", "BARCODE") putString("PLUGIN_NAME", "BARCODE")
putString("RESET_CONFIG", "true") putString("RESET_CONFIG", "true")
val isLegacyTc8000 = val props = Bundle().apply {
android.os.Build.MODEL.contains("TC8000", ignoreCase = true) putString("scanner_input_enabled", "true")
val props = Bundle().apply { // Auto-select internal scanner
putString("scanner_input_enabled", "true") putString("scanner_selection", "auto")
// Baseline that should be safe on old and new Zebra devices
putString("scanner_selection", "auto")
if (!isLegacyTc8000) {
// Newer Zebra devices
putString("scanner_selection_by_identifier", "AUTO") putString("scanner_selection_by_identifier", "AUTO")
// Hardware trigger behavior
putString("hardware_trigger_enabled", "true") putString("hardware_trigger_enabled", "true")
putString("trigger_mode", "2") // HARD trigger putString("trigger_mode", "2") // 2 = HARD trigger
// Disable Zebra's loud initial decode feedback
putString("decode_audio_feedback_uri", "") putString("decode_audio_feedback_uri", "")
putString("decode_haptic_feedback", "false") putString("decode_haptic_feedback", "false")
putString("decode_led_feedback", "false") putString("decode_led_feedback", "false")
}
}
putBundle("PARAM_LIST", props) // add in wake on trigger
} putString("trigger_wakeup_scan", "true");
}
putBundle("PARAM_LIST", props)
}
val intentConfig = Bundle().apply { val intentConfig = Bundle().apply {
putString("PLUGIN_NAME", "INTENT") putString("PLUGIN_NAME", "INTENT")

View File

@@ -1,13 +1,26 @@
import React from 'react' import React from "react";
import { Text, View } from 'react-native' import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function Logs() { export default function Logs() {
return ( const getInfo = async () => {
<View style={{ const info = "ho";
console.log(info);
};
return (
<View
style={{
flex: 1, flex: 1,
//justifyContent: "center", //justifyContent: "center",
alignItems: "center", alignItems: "center",
marginTop: 50, marginTop: 50,
}}><Text>Logs</Text></View> }}
) >
<Text>Logs</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
</View>
);
} }

View File

@@ -1,11 +1,9 @@
import React from "react"; import { View } from "react-native";
import { View } from "react-native";
import { useAppStore } from "../../hooks/useAppStore";
import ProdScanner from "../../components/ProdScanner";
import LSTScanner from "../../components/LSTScanner"; import LSTScanner from "../../components/LSTScanner";
import ProdScanner from "../../components/ProdScanner";
import { useAppStore } from "../../hooks/useAppStore";
export default function scanner() { export default function Scanner() {
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
return ( return (
<View <View
@@ -16,7 +14,11 @@ export default function scanner() {
marginTop: 50, marginTop: 50,
}} }}
> >
{parseInt(serverPort || "0", 10) >= 50000 ? <ProdScanner /> : <LSTScanner />} {parseInt(serverPort || "0", 10) >= 50000 ? (
<ProdScanner />
) : (
<LSTScanner />
)}
</View> </View>
); );
} }

View File

@@ -4,6 +4,7 @@ import { Redirect, useRouter } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native"; import { ActivityIndicator, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useServerStore } from "../hooks/useServerCheck"; import { useServerStore } from "../hooks/useServerCheck";
import { devDelay } from "../lib/devMode"; import { devDelay } from "../lib/devMode";
@@ -12,6 +13,7 @@ export default function Index() {
const [message, setMessage] = useState(<Text>Starting app...</Text>); const [message, setMessage] = useState(<Text>Starting app...</Text>);
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const setServerVersion = useServerStore((s) => s.setServerVersion); const setServerVersion = useServerStore((s) => s.setServerVersion);
//const { isUnlocked } = useMobileAuthStore();
const hasHydrated = useAppStore((s) => s.hasHydrated); const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
@@ -86,7 +88,7 @@ export default function Index() {
// TODO if theres an update go to update screen message :D // TODO if theres an update go to update screen message :D
setMessage(<Text>Opening LST scan app</Text>); setMessage(<Text>Opening LST scan app</Text>);
await devDelay(3250); await devDelay(3250);
//router.replace("/scanner");
setReady(true); setReady(true);
} catch (error) { } catch (error) {
console.log("Startup error", error); console.log("Startup error", error);
@@ -104,6 +106,9 @@ export default function Index() {
setServerVersion, setServerVersion,
]); ]);
// if (ready && !isUnlocked) {
// return <Redirect href={"/login"} />;
// }
if (ready) { if (ready) {
return <Redirect href="/(tabs)/scanner" />; return <Redirect href="/(tabs)/scanner" />;
} }

View File

@@ -0,0 +1,52 @@
import axios from "axios";
import Constants from "expo-constants";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { Input } from "../components/ui/input";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
export default function Login() {
const { setUser } = useMobileAuthStore();
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const onLogin = async () => {
try {
const res = await axios.get(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
{
timeout: 5000,
},
);
console.log(res.data);
} catch (error) {}
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<View className="flex items-center m-5">
<Text style={{ fontSize: 20, fontWeight: "600" }}>
LST Scanner Login
</Text>
<View className="w-64 p-4">
<Input
className="w-fit"
keyboardType="number-pad"
textContentType="oneTimeCode"
placeholder="Pin number"
/>
</View>
</View>
<Button title="Login" onPress={onLogin} />
</View>
);
}

View File

@@ -154,10 +154,10 @@ export default function Setup() {
marginBottom: 12, marginBottom: 12,
}} }}
> >
<Text className="text-[12] color-#666"> <Text className="text-sm color-[#312f2f]">
App v{version}-{build} App v{version}-{build}
</Text> </Text>
<Text className="text-[12] color-#666"> <Text className="text-sm color-[#312f2f]">
Server version - v{server?.versionName}-{server?.versionCode} Server version - v{server?.versionName}-{server?.versionCode}
</Text> </Text>
</View> </View>

View File

@@ -1,24 +1,51 @@
import React from 'react' import { useCallback, useEffect } from "react";
import { View, Text } from 'react-native' import { Text, View } from "react-native";
import { zebraScanner } from "../lib/ZebraScanner";
export default function LSTScanner() { export default function LSTScanner() {
return ( const handleScan = useCallback(async (scan: any) => {
<View><View style={{ alignItems: "center", margin: 10 }}> console.log(scan);
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text> }, []);
</View>
<View const clearScans = () => {
style={{ // add in
marginTop: 50, };
alignItems: "center",
}} //console.log(lastScan);
>
<Text>Relocate</Text> useEffect(() => {
<Text>0 / 4</Text> zebraScanner.ensureProfile();
</View> zebraScanner.startListening();
{/* <View> const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View>
<View style={{ alignItems: "center", margin: 10 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text>
</View>
<View
style={{
marginTop: 50,
alignItems: "center",
}}
>
<Text>Relocate</Text>
<Text>0 / 4</Text>
</View>
{/* <View>
<Text>List of recent scanned pallets TBA</Text> <Text>List of recent scanned pallets TBA</Text>
</View> */} </View> */}
</View> </View>
) );
} }

View File

@@ -0,0 +1,29 @@
import { cn } from '@/lib/utils';
import { Platform, TextInput } from 'react-native';
function Input({ className, ...props }: React.ComponentProps<typeof TextInput> & React.RefAttributes<TextInput>) {
return (
<TextInput
className={cn(
'dark:bg-input/30 border-input bg-background text-foreground flex h-10 w-full min-w-0 flex-row items-center rounded-md border px-3 py-1 text-base leading-5 shadow-sm shadow-black/5 sm:h-9',
props.editable === false &&
cn(
'opacity-50',
Platform.select({ web: 'disabled:pointer-events-none disabled:cursor-not-allowed' })
),
Platform.select({
web: cn(
'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground outline-none transition-[color,box-shadow] md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive'
),
native: 'placeholder:text-muted-foreground/50',
}),
className
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,28 @@
import { create } from "zustand";
type MobileUser = {
id: string;
name: string;
role: "user" | "lead" | "manager" | "admin";
excludedCommand: string[];
};
type AuthState = {
user: MobileUser | null;
isUnlocked: boolean;
setUser: (user: MobileUser) => void;
lock: () => void;
logout: () => void;
};
export const useMobileAuthStore = create<AuthState>((set) => ({
user: null,
isUnlocked: false,
setUser: (user) => set({ user, isUnlocked: true }),
lock: () => set({ isUnlocked: false }),
logout: () => set({ user: null, isUnlocked: false }),
}));

View File

@@ -28,9 +28,9 @@ export const zebraScanner = {
ZebraScanner.triggerScan(); ZebraScanner.triggerScan();
}, },
ensureProfile() { ensureProfile() {
ZebraScanner.ensureProfile(); ZebraScanner.ensureProfile();
}, },
addScanListener( addScanListener(
callback: (scan: ZebraScanResult) => void, callback: (scan: ZebraScanResult) => void,

View File

@@ -0,0 +1,13 @@
const roleRank = {
user: 1,
lead: 2,
manager: 3,
admin: 4,
} as const;
export function hasMobileRole(
userRole: keyof typeof roleRank,
requiredRole: keyof typeof roleRank,
) {
return roleRank[userRole] >= roleRank[requiredRole];
}

View File

@@ -0,0 +1,14 @@
CREATE TYPE "public"."mobile_role" AS ENUM('user', 'lead', 'manager', 'admin');--> statement-breakpoint
CREATE TABLE "scan_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"scanner_id" integer NOT NULL,
"pin_number" integer NOT NULL,
"pin_hash" text NOT NULL,
"excluded_commands" text DEFAULT '',
"role" "mobile_role" DEFAULT 'user' NOT NULL,
"active" boolean DEFAULT true,
"last_scan" timestamp DEFAULT now(),
"add_Date" timestamp DEFAULT now(),
"upd_date" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,3 @@
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_scanner_id_unique" UNIQUE("scanner_id");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_users_pin_number_unique" UNIQUE("pin_number");--> statement-breakpoint
ALTER TABLE "scan_users" ADD CONSTRAINT "scan_user_unique" UNIQUE("scanner_id","pin_number");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "scan_users" ALTER COLUMN "scanner_id" SET DATA TYPE text;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "pin_number" SET DATA TYPE text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -295,6 +295,27 @@
"when": 1777509638464, "when": 1777509638464,
"tag": "0041_bright_tempest", "tag": "0041_bright_tempest",
"breakpoints": true "breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1777659968051,
"tag": "0042_melted_talon",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1777664911423,
"tag": "0043_melted_lyja",
"breakpoints": true
},
{
"idx": 44,
"version": "7",
"when": 1777666145468,
"tag": "0044_steady_magneto",
"breakpoints": true
} }
] ]
} }

357
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cors": "^2.8.6", "cors": "^2.8.6",
@@ -26,6 +27,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"ldapts": "^8.1.7", "ldapts": "^8.1.7",
"modbus-serial": "^8.0.25",
"morgan": "^1.10.1", "morgan": "^1.10.1",
"mssql": "^12.2.1", "mssql": "^12.2.1",
"multer": "^2.1.1", "multer": "^2.1.1",
@@ -2116,6 +2118,263 @@
"node": ">=22" "node": ">=22"
} }
}, },
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz",
"integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "12.0.0",
"debug": "4.4.0",
"node-addon-api": "8.3.0",
"node-gyp-build": "4.8.4"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "12.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-cpp/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz",
"integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz",
"integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz",
"integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz",
"integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz",
"integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz",
"integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "13.0.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz",
"integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz",
"integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz",
"integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz",
"integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz",
"integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@simple-libs/child-process-utils": { "node_modules/@simple-libs/child-process-utils": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
@@ -2172,6 +2431,12 @@
"socket.io": ">=3.1.0" "socket.io": ">=3.1.0"
} }
}, },
"node_modules/@socket.io/admin-ui/node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/@socket.io/admin-ui/node_modules/debug": { "node_modules/@socket.io/admin-ui/node_modules/debug": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@@ -3145,10 +3410,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcryptjs": { "node_modules/bcryptjs": {
"version": "2.4.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "MIT" "license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
}, },
"node_modules/better-auth": { "node_modules/better-auth": {
"version": "1.5.5", "version": "1.5.5",
@@ -9124,6 +9392,18 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/modbus-serial": {
"version": "8.0.25",
"resolved": "https://registry.npmjs.org/modbus-serial/-/modbus-serial-8.0.25.tgz",
"integrity": "sha512-T6OHW80k7DtYZF96onavw84IXNu44EW+fybgVftWAGOraL8vTmMZod8w6thOrWj2I2qHC9Gsn2nitVTUDih+6A==",
"license": "ISC",
"dependencies": {
"debug": "^4.4.3"
},
"optionalDependencies": {
"serialport": "^13.0.0"
}
},
"node_modules/modify-values": { "node_modules/modify-values": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
@@ -9396,6 +9676,28 @@
"smart-buffer": "^4.1.0" "smart-buffer": "^4.1.0"
} }
}, },
"node_modules/node-addon-api": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz",
"integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.3", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
@@ -10741,6 +11043,53 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/serialport": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz",
"integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "13.0.0",
"@serialport/parser-byte-length": "13.0.0",
"@serialport/parser-cctalk": "13.0.0",
"@serialport/parser-delimiter": "13.0.0",
"@serialport/parser-inter-byte-timeout": "13.0.0",
"@serialport/parser-packet-length": "13.0.0",
"@serialport/parser-readline": "13.0.0",
"@serialport/parser-ready": "13.0.0",
"@serialport/parser-regex": "13.0.0",
"@serialport/parser-slip-encoder": "13.0.0",
"@serialport/parser-spacepacket": "13.0.0",
"@serialport/stream": "13.0.0",
"debug": "4.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/serialport/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"optional": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",

View File

@@ -69,6 +69,7 @@
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.6", "axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5", "better-auth": "^1.5.5",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"cors": "^2.8.6", "cors": "^2.8.6",
@@ -81,6 +82,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"ldapts": "^8.1.7", "ldapts": "^8.1.7",
"modbus-serial": "^8.0.25",
"morgan": "^1.10.1", "morgan": "^1.10.1",
"mssql": "^12.2.1", "mssql": "^12.2.1",
"multer": "^2.1.1", "multer": "^2.1.1",