47 Commits

Author SHA1 Message Date
36ac1dccb4 chore(release): 0.1.0-alpha.1
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m39s
Release and Build Image / release (push) Successful in 15s
2026-05-18 21:39:59 -05:00
514a44b6de refactor(servers): changed activeity around and trying to make use of it
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-18 21:38:08 -05:00
a7bb364a2f fix(settings): failed build due it dormant import
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 3m42s
2026-05-18 21:23:34 -05:00
047cc7cdf0 refactor(users): lots of auth stuff added to make it more easy to manage users
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 2m9s
2026-05-18 21:19:20 -05:00
8dc4d70e28 ci(app): added in chokidar to monitor folders 2026-05-18 21:18:42 -05:00
c8931c7249 fix(notifications): reprinting
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m20s
correction to external labeling

ref #20
2026-05-14 14:18:40 -05:00
67f36c5499 chore(release): 0.1.0-alpha.0
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m55s
Release and Build Image / release (push) Successful in 9s
2026-05-13 20:56:14 -05:00
ebf1060475 refactor(servers): server name now links to the actual server:port
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-13 20:53:27 -05:00
c64392f457 refactor(api): changes to call a helper api to quit and redirect if needed 2026-05-13 20:52:55 -05:00
e9e73c829c ci(servives): helpers moved around 2026-05-13 20:52:16 -05:00
bcb7773007 ci(updateserver): changes to actually add the new env stuff 2026-05-13 20:51:54 -05:00
eb950d2c29 refactor(scanner): more scanner admin stuff 2026-05-13 20:51:22 -05:00
2616acf106 fix(notification subs): made it so only acitve show
closes #14
2026-05-13 20:50:51 -05:00
30ff7b71d9 refactor(app): changed ways we get data so we can have better reasons why app no worky 2026-05-13 20:49:43 -05:00
e7af3d1182 refactor(scanner): removed 69 as an option lol 2026-05-13 20:48:43 -05:00
3e66c3920d feat(notification): migrated sql cleanup 2026-05-13 20:48:22 -05:00
eb9d77c3d4 docs(scanner): added in westbend and dayton commands to scan for updates 2026-05-13 20:47:38 -05:00
342a97f6b1 refactor(users): some user refactoring and configuring 2026-05-13 16:42:36 -05:00
b0c7277a6c docs(scanner): added in instructions on how to update the scanner
only test2 stage now commands for now.
2026-05-12 20:26:19 -05:00
dc95e50a84 ci(mobile): added in ehs config to make it more easy for users to update the scanner app on the fly 2026-05-12 12:04:19 -05:00
d2a9e1d110 fix(app): required auth was in wrong spot caused entire app to want you logged in 2026-05-12 12:02:59 -05:00
a9c69250bd refactor(mobile): scanner response
ref #16
2026-05-12 08:53:12 -05:00
d61be61f44 refactor(scanner): logging - version of app 2026-05-11 19:06:25 -05:00
f5bae2c0c2 fix(anaylistics): changes to the daily section so it populates correctly now
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m9s
2026-05-11 15:41:20 -05:00
05758791be fix(scanner): fixes to be more clear that you need to scan a command to start
closes #16
2026-05-11 15:40:49 -05:00
51026e3e2c ci(notification): removal of more console logs that shouldnt be here 2026-05-11 15:38:44 -05:00
9631736e26 chore(mobile): removed console log that shouldnt be there 2026-05-11 15:38:04 -05:00
ce9d8eaaf5 feat(scan users): added in the place to add the new scanner users in 2026-05-11 15:37:38 -05:00
1bbf5c2a49 fix(table): skelly table causing hydration error 2026-05-11 15:35:46 -05:00
13718fe702 fix(anaylitics): unique values were missing causing a weird crash 2026-05-11 14:00:54 -05:00
0de2579942 fix(scanner): changed to not crash on logging
cloases #19
2026-05-11 13:35:07 -05:00
7c31b43a4a fix(app): emit.maxlistener issue
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m27s
BREAKING CHANGE: moved teh middleware to call the api hits to the main app and removed from
everywhere else

closes #18
2026-05-11 13:25:43 -05:00
85e96f5ed1 fix(scanner): logut out corrections
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
refs #17
2026-05-11 07:59:17 -05:00
6b515c608f chore(release): 0.0.2-alpha.10
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m12s
Release and Build Image / release (push) Successful in 16s
2026-05-08 15:09:49 -05:00
d8869b103b fix(scan user): typo 2026-05-08 15:08:33 -05:00
1dba774abc chore(server): removed a console log that shouldnt be there
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 1m10s
2026-05-08 15:06:08 -05:00
505d7cea5d refactor(scan): bump in build and style update 2026-05-08 15:05:47 -05:00
1ff5e5032f test(scanusers): added in scan users as test 2026-05-08 15:05:09 -05:00
5fa70da90c chore(file): name changes.. spelled wrong 2026-05-08 15:04:31 -05:00
0459cd788a fix(spelling): corrected the spelling on the file 2026-05-08 15:03:53 -05:00
7d7d991122 fix(schema): typo in add_date 2026-05-08 15:03:33 -05:00
2721bb2a3b feat(api hits): added in api hits for monitoring 2026-05-08 15:03:03 -05:00
4424c742d2 refactor(analyitics): finished analyitics as a base 2026-05-08 15:02:34 -05:00
6d8499bfb8 ci(templates): force useage 2026-05-08 15:01:44 -05:00
9edafc9d28 feat(analytics): added in backend anaylitics 2026-05-07 10:20:50 -05:00
e9b0101095 ci(template): bug in getting the template to work correctly
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m28s
2026-05-07 09:01:15 -05:00
ca885fb01a ci(templates): added in templates for the repo to make it more easy to manage and add in new ideas
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 1m24s
2026-05-07 08:50:06 -05:00
130 changed files with 15359 additions and 424 deletions

View File

@@ -0,0 +1,66 @@
---
name: Bug Report
about: Report something that is broken or not working correctly
title: "[BUG] "
ref: "main"
labels:
- bug
---
# Summary
Briefly explain the issue.
---
# Steps To Reproduce
1. Go to ...
2. Click ...
3. Scan ...
4. Error occurs ...
---
# Expected Behavior
What should have happened?
---
# Actual Behavior
What actually happened instead?
---
# Severity
- [ ] Low
- [ ] Medium
- [ ] High
- [ ] Critical
---
# Environment
Example:
- Production
- Development
- Zebra Scanner
- Mobile Device
- Windows Server
- Docker
---
# Logs / Screenshots
Paste logs or upload screenshots here.
```txt
Paste logs here

View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,47 @@
---
name: Enhancement
about: Improve or refine an existing feature
title: "[ENHANCEMENT] "
ref: "main"
labels:
- enhancement
---
# Existing Feature
What current feature or workflow is being improved?
Example:
- Notifications
- Scanner Login
- Release Monitor
- Printing
- Auth
---
# Proposed Improvement
Describe the improvement.
---
# Expected Benefit
Why would this improvement help?
---
# Impact
- [ ] Small
- [ ] Medium
- [ ] Large
---
# Additional Notes
Anything else worth mentioning.

View File

@@ -0,0 +1,40 @@
---
name: Feature Request
about: Suggest a brand new feature or module
title: "[FEATURE] "
ref: "main"
labels:
- feature
---
# Problem Statement
What problem are you trying to solve?
---
# Proposed Solution
Describe the feature you would like added.
---
# Alternatives Considered
Any other ideas, workarounds, or approaches?
---
# Priority
- [ ] Nice to Have
- [ ] Medium Priority
- [ ] High Priority
- [ ] Critical
---
# Additional Context
Add mockups, screenshots, examples, or notes here.

3
.gitignore vendored
View File

@@ -9,8 +9,9 @@ downloads
.scriptCreds .scriptCreds
node-v24.14.0-x64.msi node-v24.14.0-x64.msi
postgresql-17.9-2-windows-x64.exe postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.exe VSCodeSetup-x64-1.120.0.exe
nssm.exe nssm.exe
frontend/.tanstack
# Logs # Logs
logs logs

View File

@@ -1,5 +1,122 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.1.0-alpha.1](https://git.tuffraid.net/cowch/lst_v3/compare/v0.1.0-alpha.0...v0.1.0-alpha.1) (2026-05-19)
### 🐛 Bug fixes
* **notifications:** reprinting ([c8931c7](https://git.tuffraid.net/cowch/lst_v3/commits/c8931c7249b8f532b5dd37df3271da98f14ee710)), closes [#20](https://git.tuffraid.net/cowch/lst_v3/issues/20)
* **settings:** failed build due it dormant import ([a7bb364](https://git.tuffraid.net/cowch/lst_v3/commits/a7bb364a2fd49d96b6195aca0cd58ba57c58f3a6))
### 🛠️ Code Refactor
* **servers:** changed activeity around and trying to make use of it ([514a44b](https://git.tuffraid.net/cowch/lst_v3/commits/514a44b6de3efe8dd8b308d98bdbc82e31ed8427))
* **users:** lots of auth stuff added to make it more easy to manage users ([047cc7c](https://git.tuffraid.net/cowch/lst_v3/commits/047cc7cdf036c39a89a0b87ab59dda8328efe0c0))
### 📈 Project changes
* **app:** added in chokidar to monitor folders ([8dc4d70](https://git.tuffraid.net/cowch/lst_v3/commits/8dc4d70e2827f0a40d2f54886fd757c8a2dc5ac4))
## [0.1.0-alpha.0](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.10...v0.1.0-alpha.0) (2026-05-14)
### ⚠ BREAKING CHANGES
* **app:** moved teh middleware to call the api hits to the main app and removed from
everywhere else
### 🌟 Enhancements
* **notification:** migrated sql cleanup ([3e66c39](https://git.tuffraid.net/cowch/lst_v3/commits/3e66c3920d65cee7a0a788f3910c1ddf09a07805))
* **scan users:** added in the place to add the new scanner users in ([ce9d8ea](https://git.tuffraid.net/cowch/lst_v3/commits/ce9d8eaaf5bcb8f53ea4bdc191347df8d589fdfa))
### 🐛 Bug fixes
* **anaylistics:** changes to the daily section so it populates correctly now ([f5bae2c](https://git.tuffraid.net/cowch/lst_v3/commits/f5bae2c0c24b85423c5c421164d94d58159ff70a))
* **anaylitics:** unique values were missing causing a weird crash ([13718fe](https://git.tuffraid.net/cowch/lst_v3/commits/13718fe70293c039bd1d9bf8cf395852e6ea6c21))
* **app:** emit.maxlistener issue ([7c31b43](https://git.tuffraid.net/cowch/lst_v3/commits/7c31b43a4a313237fa63c0c9bbc3690b74f63a6f)), closes [#18](https://git.tuffraid.net/cowch/lst_v3/issues/18)
* **app:** required auth was in wrong spot caused entire app to want you logged in ([d2a9e1d](https://git.tuffraid.net/cowch/lst_v3/commits/d2a9e1d1107ea05f13725e9528bc6ab1566c8efb))
* **notification subs:** made it so only acitve show ([2616acf](https://git.tuffraid.net/cowch/lst_v3/commits/2616acf106530f5c5ee04d1b79033795cf06b42d)), closes [#14](https://git.tuffraid.net/cowch/lst_v3/issues/14)
* **scanner:** changed to not crash on logging ([0de2579](https://git.tuffraid.net/cowch/lst_v3/commits/0de25799420f38a293ee9acc70eb36e3287145c4)), closes [#19](https://git.tuffraid.net/cowch/lst_v3/issues/19)
* **scanner:** fixes to be more clear that you need to scan a command to start ([0575879](https://git.tuffraid.net/cowch/lst_v3/commits/05758791be7a50e90b5da05d4977e618c311f654)), closes [#16](https://git.tuffraid.net/cowch/lst_v3/issues/16)
* **scanner:** logut out corrections ([85e96f5](https://git.tuffraid.net/cowch/lst_v3/commits/85e96f5ed13a81fd466c6bbff31c539244750838)), closes [#17](https://git.tuffraid.net/cowch/lst_v3/issues/17)
* **table:** skelly table causing hydration error ([1bbf5c2](https://git.tuffraid.net/cowch/lst_v3/commits/1bbf5c2a4955107a36ace05595886d19cc8e64f4))
### 📝 Chore
* **mobile:** removed console log that shouldnt be there ([9631736](https://git.tuffraid.net/cowch/lst_v3/commits/9631736e263ed00189f8118f686690cab25f09d3))
### 📚 Documentation
* **scanner:** added in instructions on how to update the scanner ([b0c7277](https://git.tuffraid.net/cowch/lst_v3/commits/b0c7277a6cdb5becec3a994ea1d5cc2d7b0326aa))
* **scanner:** added in westbend and dayton commands to scan for updates ([eb9d77c](https://git.tuffraid.net/cowch/lst_v3/commits/eb9d77c3d4767fd961759662ef44c3e09e00946b))
### 🛠️ Code Refactor
* **api:** changes to call a helper api to quit and redirect if needed ([c64392f](https://git.tuffraid.net/cowch/lst_v3/commits/c64392f45769108aa4134c7fd865f3d4bc664179))
* **app:** changed ways we get data so we can have better reasons why app no worky ([30ff7b7](https://git.tuffraid.net/cowch/lst_v3/commits/30ff7b71d9d159ced263a5330d70d53b97393157))
* **mobile:** scanner response ([a9c6925](https://git.tuffraid.net/cowch/lst_v3/commits/a9c69250bd3272ad682751e41b671c119cb678f1)), closes [#16](https://git.tuffraid.net/cowch/lst_v3/issues/16)
* **scanner:** logging - version of app ([d61be61](https://git.tuffraid.net/cowch/lst_v3/commits/d61be61f4433a2be2678d724f4724301931614c9))
* **scanner:** more scanner admin stuff ([eb950d2](https://git.tuffraid.net/cowch/lst_v3/commits/eb950d2c29f692b806d5cc4ab7014bd59a726a8d))
* **scanner:** removed 69 as an option lol ([e7af3d1](https://git.tuffraid.net/cowch/lst_v3/commits/e7af3d11824b42915cf6789f9c508a727511d678))
* **servers:** server name now links to the actual server:port ([ebf1060](https://git.tuffraid.net/cowch/lst_v3/commits/ebf1060475d37627b371bc6c79507cdde411600b))
* **users:** some user refactoring and configuring ([342a97f](https://git.tuffraid.net/cowch/lst_v3/commits/342a97f6b1054443b9126186d2c7872fbd8586da))
### 📈 Project changes
* **mobile:** added in ehs config to make it more easy for users to update the scanner app on the fly ([dc95e50](https://git.tuffraid.net/cowch/lst_v3/commits/dc95e50a8412b4fbc629fd44fcb5c77295583ca8))
* **notification:** removal of more console logs that shouldnt be here ([51026e3](https://git.tuffraid.net/cowch/lst_v3/commits/51026e3e2cce4d7f696d26aae305b3fd221f5bb1))
* **servives:** helpers moved around ([e9e73c8](https://git.tuffraid.net/cowch/lst_v3/commits/e9e73c829c2e5726650c0ac7ffa6a9055dbc982b))
* **updateserver:** changes to actually add the new env stuff ([bcb7773](https://git.tuffraid.net/cowch/lst_v3/commits/bcb7773007894ac2f85fe2a0b47faf14c7b474ad))
## [0.0.2-alpha.10](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.9...v0.0.2-alpha.10) (2026-05-08)
### 🌟 Enhancements
* **analytics:** added in backend anaylitics ([9edafc9](https://git.tuffraid.net/cowch/lst_v3/commits/9edafc9d2810f339d197c10dfc6a037b3352d81f))
* **api hits:** added in api hits for monitoring ([2721bb2](https://git.tuffraid.net/cowch/lst_v3/commits/2721bb2a3bf1f829591d26a0716f74c4f7fc0c79))
* **scanner:** added in lanechecks ([87803ee](https://git.tuffraid.net/cowch/lst_v3/commits/87803eed43069b73de3f66e6524bb45da9c46334))
### 🐛 Bug fixes
* **scan user:** typo ([d8869b1](https://git.tuffraid.net/cowch/lst_v3/commits/d8869b103b80e4208b3928a370a9524ef33d25cd))
* **schema:** typo in add_date ([7d7d991](https://git.tuffraid.net/cowch/lst_v3/commits/7d7d9911223905d6767b87d2471b6607a90f1ea7))
* **spelling:** corrected the spelling on the file ([0459cd7](https://git.tuffraid.net/cowch/lst_v3/commits/0459cd788aaad6ac54a67e23f798ce5e5a437394))
### 📝 Chore
* **file:** name changes.. spelled wrong ([5fa70da](https://git.tuffraid.net/cowch/lst_v3/commits/5fa70da90ca290ee45088e9c8eb06ba48a6677af))
* **server:** removed a console log that shouldnt be there ([1dba774](https://git.tuffraid.net/cowch/lst_v3/commits/1dba774abc54bf20850c3f26d49926e86d59712d))
### 🛠️ Code Refactor
* **analyitics:** finished analyitics as a base ([4424c74](https://git.tuffraid.net/cowch/lst_v3/commits/4424c742d24dc230b2bc1782e33535184c378cf0))
* **scan:** bump in build and style update ([505d7ce](https://git.tuffraid.net/cowch/lst_v3/commits/505d7cea5d2f52fc4a3ec1edff1878be703c4034))
* **scanner:** added toasts in to make it look better ([edb3668](https://git.tuffraid.net/cowch/lst_v3/commits/edb366854825f4c24ab5d77cf88759465d067f00))
### 📝 Testing Code
* **scanusers:** added in scan users as test ([1ff5e50](https://git.tuffraid.net/cowch/lst_v3/commits/1ff5e5032f9c8bf81f972dc99d6c86ba8d3936c6))
### 📈 Project changes
* **template:** bug in getting the template to work correctly ([e9b0101](https://git.tuffraid.net/cowch/lst_v3/commits/e9b01010954624aed738cd6e4b82fccbba195cc4))
* **templates:** added in templates for the repo to make it more easy to manage and add in new ideas ([ca885fb](https://git.tuffraid.net/cowch/lst_v3/commits/ca885fb01a3c8bc22694c2e05269c43fcd4de70e))
* **templates:** force useage ([6d8499b](https://git.tuffraid.net/cowch/lst_v3/commits/6d8499bfb85f7b9131b1ec7b31a17c4256d0f0cf))
## [0.0.2-alpha.9](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.8...v0.0.2-alpha.9) (2026-05-06) ## [0.0.2-alpha.9](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.8...v0.0.2-alpha.9) (2026-05-06)

View File

@@ -1,12 +1,16 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import build from "./admin.build.js"; import build from "./admin.build.js";
import update from "./admin.updateServer.js"; import update from "./admin.updateServer.js";
import users from "./admin.users.js";
export const setupAdminRoutes = (baseUrl: string, app: Express) => { export const setupAdminRoutes = (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/admin/build`, requireAuth, build); app.use(`${baseUrl}/api/admin/build`, requireAuth, build);
app.use(`${baseUrl}/api/admin/build`, requireAuth, update); app.use(`${baseUrl}/api/admin/build`, requireAuth, update);
app.use(`${baseUrl}/api/admin/user`, requireAuth, users);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -0,0 +1,46 @@
/**
* To be able to run this we need to set our dev pc in the .env.
* if its empty just ignore it. this will just be the double catch
*/
import { fromNodeHeaders } from "better-auth/node";
import { Router } from "express";
import { auth } from "../utils/auth.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
r.get("/", async (req, res) => {
const { users } = await auth.api.listUsers({
query: {
limit: 50,
},
headers: fromNodeHeaders(req.headers),
});
// console.log(error);
// if (error) {
// return apiReturn(res, {
// success: false,
// level: "info",
// module: "admin",
// subModule: "user",
// message: `There was an error getting the users.`,
// data: users,
// status: 400,
// });
// }
return apiReturn(res, {
success: true,
level: "info",
module: "admin",
subModule: "users",
message: `Current active users.`,
data: users,
status: 200,
});
});
export default r;

View File

@@ -3,7 +3,9 @@ import { fileURLToPath } from "node:url";
import { toNodeHandler } from "better-auth/node"; import { toNodeHandler } from "better-auth/node";
import express from "express"; import express from "express";
import morgan from "morgan"; import morgan from "morgan";
import { umamiConfig } from "./configs/umami.config.js";
import { createLogger } from "./logger/logger.controller.js"; import { createLogger } from "./logger/logger.controller.js";
import { routeHitMiddleware } from "./middleware/routeHit.middleware.js";
import { setupRoutes } from "./routeHandler.routes.js"; import { setupRoutes } from "./routeHandler.routes.js";
import { auth } from "./utils/auth.utils.js"; import { auth } from "./utils/auth.utils.js";
import { lstCors } from "./utils/cors.utils.js"; import { lstCors } from "./utils/cors.utils.js";
@@ -29,8 +31,26 @@ const createApp = async () => {
app.use(morgan("dev")); app.use(morgan("dev"));
app.set("trust proxy", true); app.set("trust proxy", true);
app.use(lstCors()); app.use(lstCors());
app.use(routeHitMiddleware);
app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth)); app.all(`${baseUrl}/api/auth/*splat`, toNodeHandler(auth));
app.use(express.json()); app.use(express.json());
app.get(`${baseUrl}/api/lst-config.js`, (_, res) => {
res.type("application/javascript");
res.setHeader("Cache-Control", "no-store");
res.send(`
window.LST_CONFIG = {
appName: ${JSON.stringify(umamiConfig.appName ?? "LST")},
site: ${JSON.stringify(umamiConfig.site ?? "unknown")},
server: ${JSON.stringify(umamiConfig.server ?? "unknown")},
appVersion: ${JSON.stringify(umamiConfig.appVersion ?? "dev")},
umamiHost: ${JSON.stringify(umamiConfig.umamiHost ?? "")},
umamiWebsiteId: ${JSON.stringify(umamiConfig.umamiWebsiteId ?? "")}
};
`);
});
setupRoutes(baseUrl, app); setupRoutes(baseUrl, app);
app.use( app.use(

View File

@@ -1,9 +1,11 @@
import type { Express } from "express"; import type { Express } from "express";
import login from "./login.route.js"; import login from "./login.route.js";
import register from "./register.route.js"; import register from "./register.route.js";
export const setupAuthRoutes = (baseUrl: string, app: Express) => { export const setupAuthRoutes = (baseUrl: string, app: Express) => {
//setup all the routes //setup all the routes
app.use(`${baseUrl}/api/authentication/login`, login); app.use(`${baseUrl}/api/authentication/login`, login);
app.use(`${baseUrl}/api/authentication/register`, register); app.use(`${baseUrl}/api/authentication/register`, register);
}; };

View File

@@ -0,0 +1,21 @@
export type UmamiRuntimeConfig = {
appName: string;
site: string;
server: string;
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
};
export const umamiConfig: UmamiRuntimeConfig = {
appName: process.env.APP_NAME ?? "LST",
site: process.env.URL ?? "unknown",
server: process.env.PROD_PLANT_TOKEN ?? "unknown", // could also be server name based on our setup.
appVersion: process.env.NODE_ENV ?? "dev",
umamiHost: process.env.UMAMI_HOST ?? "",
umamiWebsiteId: process.env.UMAMI_WEBSITE_ID ?? "",
};
export function isUmamiEnabled() {
return Boolean(umamiConfig.umamiHost && umamiConfig.umamiWebsiteId);
}

View File

@@ -0,0 +1,21 @@
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const analytics = pgTable("analytics", {
id: uuid("id").defaultRandom().primaryKey(),
createdAt: timestamp("created_at").defaultNow().notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
actualPath: text("actual_path").notNull(),
statusCode: integer("status_code").notNull(),
durationMs: integer("duration_ms").notNull(),
module: text("module"),
userId: text("user_id"),
userEmail: text("user_email"),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
});

View File

@@ -0,0 +1,45 @@
import {
date,
integer,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
export const analyticsDaily = pgTable(
"analytics_daily",
{
id: uuid("id").defaultRandom().primaryKey(),
businessDate: date("business_date", { mode: "string" }).notNull(),
method: text("method").notNull(),
routePattern: text("route_pattern").notNull(),
module: text("module").notNull(),
totalHits: integer("total_hits").notNull(),
uniqueUsers: integer("unique_users").notNull(),
successCount: integer("success_count").notNull(),
errorCount: integer("error_count").notNull(),
avgDurationMs: integer("avg_duration_ms").notNull(),
maxDurationMs: integer("max_duration_ms").notNull(),
firstHitAt: timestamp("first_hit_at").notNull(),
lastHitAt: timestamp("last_hit_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
unique("analytics_daily_business_route_unique").on(
table.businessDate,
table.method,
table.routePattern,
table.module,
),
],
);

View File

@@ -11,8 +11,9 @@ export const scanLog = pgTable("scan_log", {
commandDescription: text("command_description"), commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"), runningNumber: text("running_number").default("0"),
status: text("status"), status: text("status"),
scannerVersion: text("scanner_version").default("0"),
lines: jsonb("lines").default([]), lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(), add_Date: timestamp("add_date").defaultNow(),
}); });
export const scanLogSchema = createSelectSchema(scanLog); export const scanLogSchema = createSelectSchema(scanLog);

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./gpSqlRestart.route.js"; import restart from "./gpSqlRestart.route.js";
import start from "./gpSqlStart.route.js"; import start from "./gpSqlStart.route.js";
import stop from "./gpSqlStop.route.js"; import stop from "./gpSqlStop.route.js";
@@ -7,11 +8,10 @@ export const setupGPSqlRoutes = (baseUrl: string, app: Express) => {
//setup all the routes //setup all the routes
// Apply auth to entire router // Apply auth to entire router
const router = Router(); const router = Router();
router.use(requireAuth);
router.use(start); router.use(start);
router.use(stop); router.use(stop);
router.use(restart); router.use(restart);
app.use(`${baseUrl}/api/system/gpSql`, router); app.use(`${baseUrl}/api/system/gpSql`, requireAuth, router);
}; };

View File

@@ -0,0 +1,83 @@
// routeHit.middleware.ts
import type { NextFunction, Request, Response } from "express";
import {
createRouteHit,
shouldIgnoreRoute,
} from "../utils/analyticRouteHits.utils.js";
export function routeHitMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const start = performance.now();
res.on("finish", () => {
const actualPath = getActualPath(req);
if (shouldIgnoreRoute(actualPath)) {
return;
}
const durationMs = Math.round(performance.now() - start);
const routePattern = getRoutePattern(req) as string;
const module = getModuleName(req);
void createRouteHit({
method: req.method,
routePattern,
actualPath,
statusCode: res.statusCode,
durationMs,
module,
// adjust these names to your Better Auth/session shape
userId: req.user?.id ?? null,
userEmail: req.user?.email ?? null,
ipAddress: req.ip ?? null,
userAgent: req.get("user-agent") ?? null,
}).catch((err) => {
console.error("Failed to save route hit", err);
});
});
next();
}
function getActualPath(req: Request) {
return req.originalUrl.split("?")[0] ?? req.path ?? "unknown";
}
function getRoutePattern(req: Request) {
const baseUrl = req.baseUrl || "";
const routePath = req.route?.path;
if (typeof routePath === "string") {
return `${baseUrl}${routePath}`;
}
return getActualPath(req);
}
function getModuleName(req: Request) {
const path = req.originalUrl.split("?")[0];
if (path?.includes("/printers")) return "printers";
if (path?.includes("/releases")) return "releases";
if (path?.includes("/quality")) return "quality";
if (path?.includes("/scanner")) return "scanner";
if (path?.includes("/settings")) return "settings";
if (path?.includes("/users")) return "users";
if (path?.includes("/mobile")) return "mobile";
if (path?.includes("/servers")) return "servers";
if (path?.includes("/logistics")) return "servers";
if (path?.includes("/ocp")) return "ocp";
if (path?.includes("/auth")) return "auth";
if (path?.includes("/datamart")) return "datamart";
if (path?.includes("/opendock")) return "opendock";
return "unknown";
}

View File

@@ -0,0 +1,54 @@
import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { settings } from "../db/schema/settings.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
const r = Router();
// scanners that are dedicated to specific users.
const SPECIAL_SCANNERS = [98];
const buildAllowedScannerIds = (scannerCount: number) => {
const generatedIds = Array.from({ length: scannerCount }, (_, i) => i + 1);
return Array.from(new Set([...generatedIds, ...SPECIAL_SCANNERS])).sort(
(a, b) => a - b,
);
};
r.get("/", async (_, res) => {
// get the scan users and setting
const scanusers = await db.select().from(scanUser);
const scannerIdSetting = await db
.select()
.from(settings)
.where(eq(settings.name, "scannerIds"));
const usedScannerIds = scanusers.map((x) => Number(x.scannerId));
const allowedScannerIds = buildAllowedScannerIds(
Number(scannerIdSetting[0]?.value ?? 0),
);
const availableScannerIds = allowedScannerIds.filter(
(id) => !usedScannerIds.includes(id),
);
const data = availableScannerIds.map((id) => ({
label: `${id}`,
value: id,
}));
return apiReturn(res, {
success: true,
level: "info",
module: "mobile",
subModule: "scanner",
message: `There are ${availableScannerIds.length} scanner id's`,
data,
status: 200,
});
});
export default r;

View File

@@ -1,46 +0,0 @@
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,105 @@
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);
});
router.get("/ehs/xml", (_, res) => {
const xmlPath = path.join(downloadDir, "enterprisehomescreen.xml");
if (!fs.existsSync(xmlPath)) {
return res.status(404).json({
success: false,
message: "EHS XML not found",
});
}
res.setHeader("Content-Type", "application/xml");
res.setHeader(
"Content-Disposition",
`attachment; filename="enterprisehomescreen.xml"`,
);
return res.sendFile(xmlPath);
});
router.get("/upgrade/android/13", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_13-51-16.00-TG-U00-STD-HEL-04.zip",
);
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-Type", "application/zip");
res.setHeader(
"Content-Disposition",
`attachment; filename="HE_FULL_UPDATE_13.zip"`,
);
return res.sendFile(apkPath);
});
router.get("/upgrade/android/14", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_14-38-04.00-UG-U15-STD-HEL-04.zip",
);
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-Type", "application/zip");
res.setHeader(
"Content-Disposition",
`attachment; filename="HE_FULL_UPDATE_14.zip"`,
);
return res.sendFile(apkPath);
});
export default router;

View File

@@ -1,6 +1,12 @@
import { Router } from "express"; import { Router } from "express";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { runProdApi } from "../utils/prodEndpoint.utils.js"; import { runProdApi } from "../utils/prodEndpoint.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn, returnFunc } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const router = Router(); const router = Router();
@@ -9,7 +15,27 @@ router.post("/", async (req, res) => {
const lane = body.lane.split("#"); const lane = body.lane.split("#");
console.log(lane[2]); // check if the plant has warehousing activated
const featureQ = sqlQuerySelector(`featureCheck`) as SqlQuery;
const { data: fd, error: fe } = await tryCatch(
prodQuery(featureQ.query, `Running feature check`),
);
if (fe) {
return returnFunc({
success: false,
level: "error",
module: "datamart",
subModule: "query",
message: `feature check failed`,
data: fe as any,
notify: false,
});
}
console.log(fd);
const laneData = await runProdApi({ const laneData = await runProdApi({
method: "post", method: "post",
endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits", endpoint: "/public/v1.1/Warehousing/GetWarehouseUnits",

View File

@@ -1,5 +1,7 @@
import type { Express } from "express"; import type { Express } from "express";
import downloads from "./donwloadApps.route.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import available from "./availableScanIds.route.js";
import downloads from "./downloadApps.route.js";
import lanes from "./laneCheck.js"; import lanes from "./laneCheck.js";
import authPin from "./mobileAuth.route.js"; import authPin from "./mobileAuth.route.js";
import newPin from "./mobilePin.route.js"; import newPin from "./mobilePin.route.js";
@@ -8,12 +10,14 @@ import version from "./version.route.js";
export const setupMobileRoutes = (baseUrl: string, app: Express) => { export const setupMobileRoutes = (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/mobile/version`, version);
app.use(`${baseUrl}/api/mobile/apk`, downloads); app.use(`${baseUrl}/api/mobile/version`, featureCheck("mobile"), version);
app.use(`${baseUrl}/api/mobile/logs`, logs); app.use(`${baseUrl}/api/mobile/apk`, featureCheck("mobile"), downloads);
app.use(`${baseUrl}/api/mobile/auth`, authPin); app.use(`${baseUrl}/api/mobile/logs`, featureCheck("mobile"), logs);
app.use(`${baseUrl}/api/mobile/pin`, newPin); app.use(`${baseUrl}/api/mobile/auth`, featureCheck("mobile"), authPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, lanes); app.use(`${baseUrl}/api/mobile/pin`, featureCheck("mobile"), newPin);
app.use(`${baseUrl}/api/mobile/laneCheck`, featureCheck("mobile"), lanes);
app.use(`${baseUrl}/api/mobile/available`, featureCheck("mobile"), available);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -162,6 +162,14 @@ r.post("/user", async (req, res) => {
r.get("/user", requireAuth, async (_, res) => { r.get("/user", requireAuth, async (_, res) => {
const { data, error } = await tryCatch(db.select().from(scanUser)); const { data, error } = await tryCatch(db.select().from(scanUser));
// await trackLstEvent({
// eventName: "mobile_get_users",
// url: "/mobile/users",
// eventData: {
// module: "mobile",
// },
// });
if (error) { if (error) {
return apiReturn(res, { return apiReturn(res, {
success: false, success: false,

View File

@@ -1,25 +1,34 @@
import { eq, sql } from "drizzle-orm";
import { Router } from "express"; import { Router } from "express";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { scanLog } from "../db/schema/scanlog.schema.js"; import { scanLog } from "../db/schema/scanlog.schema.js";
import { scanUser } from "../db/schema/scanUsers.js";
import { apiReturn } from "../utils/returnHelper.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js";
const router = Router(); const router = Router();
router.post("/", async (req, res) => { router.post("/", async (req, res) => {
const body = req.body; const body = req.body;
try {
await db
.update(scanUser)
.set({ lastScan: sql`NOW()` })
.where(eq(scanUser.name, body.name));
} catch (error) {
console.log(error);
}
const newLog = await db const newLog = await db
.insert(scanLog) .insert(scanLog)
.values({ .values({
scannerId: body.scannerId, scannerId: body.scannerId ?? "",
message: body.message, message: body.message ?? "",
prompt: body.prompt, prompt: body.prompt ?? "",
commandDescription: body.commandDescription, commandDescription: body.commandDescription ?? "",
status: body.status, status: body.status ?? "",
lines: body.lines, lines: body.lines ?? "",
user: body.user, user: body.user ?? "",
runningNumber: body.runningNumber, runningNumber: body.runningNumber ?? "",
scannerVersion: body.scannerVersion ?? "0",
}) })
.returning(); .returning();

View File

@@ -1,7 +1,9 @@
import fs from "node:fs"; import fs from "node:fs";
import { and, eq } from "drizzle-orm";
import { Router } from "express"; import { Router } from "express";
import path from "path"; import path from "path";
import { db } from "../db/db.controller.js";
import { settings } from "../db/schema/settings.schema.js";
const router = Router(); const router = Router();
@@ -10,6 +12,15 @@ const appJsonPath = path.join(projectRoot, "app.json");
router.get("/", async (req, res) => { router.get("/", async (req, res) => {
const baseUrl = `${req.protocol}://${req.get("host")}`; const baseUrl = `${req.protocol}://${req.get("host")}`;
const mobileSettings = await db
.select()
.from(settings)
.where(
and(
eq(settings.moduleName, "mobile"),
eq(settings.settingType, "standard"),
),
);
const raw = fs.readFileSync(appJsonPath, "utf-8"); const raw = fs.readFileSync(appJsonPath, "utf-8");
const config = JSON.parse(raw); const config = JSON.parse(raw);
@@ -22,6 +33,7 @@ router.get("/", async (req, res) => {
versionCode: exp.android?.versionCode, versionCode: exp.android?.versionCode,
minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0, minSupportedVersionCode: exp?.android?.minSupportedVersionCode ?? 0,
downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`, downloadUrl: `${baseUrl}/lst/api/mobile/apk/latest`,
settings: mobileSettings,
}); });
}); });

View File

@@ -0,0 +1,80 @@
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
// disable the jobs
const jobNames: string[] = [
"monitor_$_lots",
"monitor_$_lots_2",
"monitor$lots",
"Monitor_APO", //listen for people to cry this is no longer a thing
"Monitor_APO2",
"Monitor_AutoConsumeMaterials", // TODO: migrate to lst
"Monitor_AutoConsumeMaterials_iow1",
"Monitor_AutoConsumeMaterials_iow2",
"Monitor_BlockedINV_Loc",
"monitor_inv_cycle",
"monitor_inv_cycle_1",
"monitor_inv_cycle_2",
"monitor_edi_import", // TODO: migrate to lst -- for the query select count(*) from AlplaPROD_test3.dbo.T_EDIDokumente (nolock) where /* IdLieferant > 1 and */ add_date > DATEADD(MINUTE, -30, getdate())
"Monitor_Lot_Progression",
"Monitor_Lots", // TODO: migrate to lst -- this should be the one where we monitor the when a lot is assigned if its missing some data.
"Monitor_MinMax", // TODO:Migrate to lst
"Monitor_MinMax_iow2",
"Monitor_PM",
"Monitor_Purity",
"monitor_wastebookings", // TODO: Migrate
"LastPriceUpdate", // not even sure what this is
"GETLabelsCount", // seems like an old jc job
"jobforpuritycount", // was not even working correctly
"Monitor_EmptyAutoConsumLocations", // not sure who uses this one
"monitor_labelreprint", // Migrated but need to find out who really wants this
"test", // not even sure why this is active
"UpdateLastMoldUsed", // old jc inserts data into a table but not sure what its used for not linked to any other alert
"UpdateWhsePositions3", // old jc inserts data into a table but not sure what its used for not linked to any other alert
"UpdateWhsePositions4",
"delete_print", // i think this was in here for when we was having lag prints in iowa1
"INV_WHSE_1", // something random i wrote long time ago looks like an inv thing to see aged stuff
"INV_WHSE_2",
"laneAgeCheck", // another strange one thats been since moved to lst
"monitor_blocking_2",
"monitor_blocking", // already in lst
"monitor_min_inv", // do we still want this one? it has a description of: this checks m-f the min inventory of materials based on the min level set in stock
"Monitor_MixedLocations",
"Monitor_PM",
"Monitor_PM2",
"wrong_lots_1",
"wrong_lots_2",
"invenotry check", // spelling error one of my stupids
"monitor_hold_monitor",
"Monitor_Silo_adjustments",
"monitor_qualityLocMonitor", // validating with lima this is still needed
"Monitor_Stock_Change",
];
export const sqlJobCleanUp = async () => {
// running a query to disable jobs that are moved to lst to be better maintained
const sqlQuery = sqlQuerySelector("disableJob") as SqlQuery;
if (!sqlQuery.success) {
console.error("Failed to load the query: ", sqlQuery.message);
return;
}
for (const job of jobNames) {
const { error } = await tryCatch(
prodQuery(
sqlQuery.query.replace("[jobName]", `${job}`),
`Disabling job: ${job}`,
),
);
if (error) {
console.error(error);
}
//console.log(data);
}
};

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import manual from "./notification.manualTrigger.js"; import manual from "./notification.manualTrigger.js";
import getNotifications from "./notification.route.js"; import getNotifications from "./notification.route.js";
import updateNote from "./notification.update.route.js"; import updateNote from "./notification.update.route.js";
@@ -10,13 +11,48 @@ import updateSub from "./notificationSub.update.route.js";
export const setupNotificationRoutes = (baseUrl: string, app: Express) => { export const setupNotificationRoutes = (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/notification`, requireAuth, getNotifications); app.use(
app.use(`${baseUrl}/api/notification`, requireAuth, updateNote); `${baseUrl}/api/notification`,
app.use(`${baseUrl}/api/notification/manual`, requireAuth, manual); requireAuth,
app.use(`${baseUrl}/api/notification/sub`, requireAuth, subs);
app.use(`${baseUrl}/api/notification/sub`, requireAuth, newSub); getNotifications,
app.use(`${baseUrl}/api/notification/sub`, requireAuth, updateSub); );
app.use(`${baseUrl}/api/notification/sub`, requireAuth, deleteSub); app.use(
`${baseUrl}/api/notification`,
requireAuth,
updateNote,
);
app.use(
`${baseUrl}/api/notification/manual`,
requireAuth,
manual,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
subs,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
newSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
updateSub,
);
app.use(
`${baseUrl}/api/notification/sub`,
requireAuth,
deleteSub,
);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -43,7 +43,7 @@ const parseZebraAlert = (body: any): PrinterEvent => {
}; };
}; };
r.post("/printer/listener/:printer", upload.any(), async (req, res) => { r.post("/:printer", upload.any(), async (req, res) => {
const { printer: printerName } = req.params; const { printer: printerName } = req.params;
const event: PrinterEvent = parseZebraAlert(req.body); const event: PrinterEvent = parseZebraAlert(req.body);

View File

@@ -21,7 +21,7 @@ import { printerSync } from "./ocp.printer.manage.js";
const r = Router(); const r = Router();
r.post("/printer/update", async (_, res) => { r.post("/update", async (_, res) => {
printerSync(); printerSync();
return apiReturn(res, { return apiReturn(res, {
success: true, success: true,

View File

@@ -1,24 +1,16 @@
import { type Express, Router } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import listener from "./ocp.printer.listener.js"; import listener from "./ocp.printer.listener.js";
import update from "./ocp.printer.update.js"; import update from "./ocp.printer.update.js";
export const setupOCPRoutes = (baseUrl: string, app: Express) => { export const setupOCPRoutes = (baseUrl: string, app: Express) => {
//setup all the routes app.use(`${baseUrl}/api/ocp/printer/listener`, featureCheck("ocp"), listener);
const router = Router(); app.use(
`${baseUrl}/api/ocp/printer`,
// is the feature even on? featureCheck("ocp"),
router.use(featureCheck("ocp")); requireAuth,
update,
// non auth routes up here );
router.use(listener);
// auth routes below here
router.use(requireAuth);
router.use(update);
//router.use("");
app.use(`${baseUrl}/api/ocp`, router);
}; };

View File

@@ -1,19 +1,16 @@
import { type Express, Router } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js"; import { featureCheck } from "../middleware/featureActive.middleware.js";
import getApt from "./opendockGetRelease.route.js"; import getApt from "./opendockGetRelease.route.js";
export const setupOpendockRoutes = (baseUrl: string, app: Express) => { export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
//setup all the routes //setup all the routes
// Apply auth to entire router
const router = Router();
// is the feature even on? app.use(
router.use(featureCheck("opendock_sync")); `${baseUrl}/api/opendock`,
featureCheck("opendock_sync"),
// we need to make sure we are authenticated to see the releases requireAuth,
router.use(requireAuth); getApt,
);
router.use(getApt);
app.use(`${baseUrl}/api/opendock`, router);
}; };

View File

@@ -1,5 +1,6 @@
import { type Express, Router } from "express"; import { type Express, Router } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./prodSqlRestart.route.js"; import restart from "./prodSqlRestart.route.js";
import start from "./prodSqlStart.route.js"; import start from "./prodSqlStart.route.js";
import stop from "./prodSqlStop.route.js"; import stop from "./prodSqlStop.route.js";
@@ -9,9 +10,7 @@ export const setupProdSqlRoutes = (baseUrl: string, app: Express) => {
const router = Router(); const router = Router();
router.use(requireAuth); router.use(requireAuth);
router.use(start); app.use(`${baseUrl}/api/system/prodSql/start`, requireAuth, start);
router.use(stop); app.use(`${baseUrl}/api/system/prodSql/stop`, requireAuth, stop);
router.use(restart); app.use(`${baseUrl}/api/system/prodSql/restart`, requireAuth, restart);
app.use(`${baseUrl}/api/system/prodSql`, router);
}; };

View File

@@ -4,7 +4,7 @@ import { closePool, connectProdSql } from "./prodSqlConnection.controller.js";
const r = Router(); const r = Router();
r.post("/restart", async (_, res) => { r.post("/", async (_, res) => {
await closePool(); await closePool();
await new Promise((r) => setTimeout(r, 2000)); await new Promise((r) => setTimeout(r, 2000));

View File

@@ -4,7 +4,7 @@ import { connectProdSql } from "./prodSqlConnection.controller.js";
const r = Router(); const r = Router();
r.post("/start", async (_, res) => { r.post("/", async (_, res) => {
const connect = await connectProdSql(); const connect = await connectProdSql();
apiReturn(res, { apiReturn(res, {
success: connect.success, success: connect.success,

View File

@@ -4,7 +4,7 @@ import { closePool } from "./prodSqlConnection.controller.js";
const r = Router(); const r = Router();
r.post("/stop", async (_, res) => { r.post("/", async (_, res) => {
const connect = await closePool(); const connect = await closePool();
apiReturn(res, { apiReturn(res, {
success: connect.success, success: connect.success,

View File

@@ -0,0 +1,8 @@
/*
disables sql jobs.
*/
EXEC msdb.dbo.sp_update_job @job_name = N'[jobName]', @enabled = 0;
-- DECLARE @JobName varchar(max) = '[jobName]'
-- UPDATE msdb.dbo.sysjobs
-- SET enabled = 0
-- WHERE name = @JobName;

View File

@@ -1,16 +1,17 @@
use [test1_AlplaPROD2.0_Read] use [test1_AlplaPROD2.0_Read]
SELECT SELECT
--JSON_VALUE(content, '$.EntityId') as labelId JSON_VALUE(content, '$.EntityId') as labelId,
a.id a.id
,ActorName ,ActorName
,FORMAT(PrintDate, 'yyyy-MM-dd HH:mm') as printDate --,FORMAT(l.PrintDate, 'yyyy-MM-dd HH:mm') as printDate
,Format(COALESCE(l.PrintDate, e.ProductionDate), 'yyyy-MM-dd HH:mm') as printDate
,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime ,FORMAT(CreatedDateTime, 'yyyy-MM-dd HH:mm') createdDateTime
,l.ArticleHumanReadableId as av ,COALESCE(l.ArticleHumanReadableId,e.ArticleHumanReadableId) as av
,l.ArticleDescription as alias ,COALESCE(l.ArticleDescription, av.Name) as alias
,PrintedCopies ,COALESCE(l.PrintedCopies, 0) as PrintedCopies
,p.name as printerName ,COALESCE(p.name,'External Label not tracked') as printerName
,RunningNumber ,COALESCE(l.RunningNumber, e.RunningNumber) as runningNumber
--,* --,*
FROM [support].[AuditLog] (nolock) as a FROM [support].[AuditLog] (nolock) as a
@@ -18,10 +19,20 @@ left join
[labelling].[InternalLabel] (nolock) as l on [labelling].[InternalLabel] (nolock) as l on
l.id = JSON_VALUE(content, '$.EntityId') l.id = JSON_VALUE(content, '$.EntityId')
OUTER APPLY (
SELECT TOP 1 *
FROM labelling.ExternalLabel e
WHERE e.id = JSON_VALUE(a.content, '$.EntityId')
ORDER BY e.Id DESC
) e
left join left join
[masterData].[printer] (nolock) as p on [masterData].[printer] (nolock) as p on
p.id = l.PrinterId p.id = l.PrinterId
left join
[masterData].[article] (nolock) as av on
av.HumanReadableId = e.ArticleHumanReadableId
where message like '%reprint%' where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET()) and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList] and a.id > [ignoreList]

View File

@@ -16,6 +16,7 @@ import { setupUtilsRoutes } from "./utils/utils.routes.js";
export const setupRoutes = (baseUrl: string, app: Express) => { export const setupRoutes = (baseUrl: string, app: Express) => {
//routes that are on by default //routes that are on by default
setupMobileRoutes(baseUrl, app);
setupSystemRoutes(baseUrl, app); setupSystemRoutes(baseUrl, app);
setupAdminRoutes(baseUrl, app); setupAdminRoutes(baseUrl, app);
setupApiDocsRoutes(baseUrl, app); setupApiDocsRoutes(baseUrl, app);
@@ -28,5 +29,4 @@ 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

@@ -8,6 +8,7 @@ import { connectGPSql } from "./gpSql/gpSqlConnection.controller.js";
import { createLogger } from "./logger/logger.controller.js"; import { createLogger } from "./logger/logger.controller.js";
import { historicalSchedule } from "./logistics/logistics.historicalInv.js"; import { historicalSchedule } from "./logistics/logistics.historicalInv.js";
import { startNotifications } from "./notification/notification.controller.js"; import { startNotifications } from "./notification/notification.controller.js";
import { sqlJobCleanUp } from "./notification/notification.SqlJobCleanUp.js";
import { createNotifications } from "./notification/notifications.master.js"; import { createNotifications } from "./notification/notifications.master.js";
import { printerSync } from "./ocp/ocp.printer.manage.js"; import { printerSync } from "./ocp/ocp.printer.manage.js";
import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js"; import { monitorReleaseChanges } from "./opendock/openDockRreleaseMonitor.utils.js";
@@ -18,6 +19,11 @@ import { setupSocketIORoutes } from "./socket.io/serverSetup.js";
import { serversChecks } from "./system/serverData.controller.js"; import { serversChecks } from "./system/serverData.controller.js";
import { baseSettingValidationCheck } from "./system/settingsBase.controller.js"; import { baseSettingValidationCheck } from "./system/settingsBase.controller.js";
import { startTCPServer } from "./tcpServer/tcp.server.js"; import { startTCPServer } from "./tcpServer/tcp.server.js";
import {
aggregateRouteHitsForBusinessDay,
cleanupOldRouteHits,
runRouteHitAnalyticsCron,
} from "./utils/analyticRouteHits.utils.js";
import { createCronJob } from "./utils/croner.utils.js"; import { createCronJob } from "./utils/croner.utils.js";
import { sendEmail } from "./utils/sendEmail.utils.js"; import { sendEmail } from "./utils/sendEmail.utils.js";
@@ -68,10 +74,19 @@ const start = async () => {
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120)); createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
historicalSchedule(); historicalSchedule();
createCronJob("aggregateHits", "0 0 7 * * *", async () =>
runRouteHitAnalyticsCron(),
);
createCronJob("cleanHitsUp", "0 0 7 * * *", () => cleanupOldRouteHits());
// one shots only needed to run on server startups // one shots only needed to run on server startups
createNotifications(); createNotifications();
startNotifications(); startNotifications();
serversChecks(); serversChecks();
aggregateRouteHitsForBusinessDay();
// can be removed at a later date
sqlJobCleanUp();
}, 5 * 1000); }, 5 * 1000);
process.on("uncaughtException", async (err) => { process.on("uncaughtException", async (err) => {

View File

@@ -76,6 +76,16 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "mobile",
value: "0",
active: false,
description: "LST Android Mobile app",
moduleName: "mobile",
settingType: "feature",
roles: ["admin"],
seedVersion: 1,
},
// standard settings // standard settings
{ {
@@ -304,6 +314,49 @@ const newSettings: NewSetting[] = [
roles: ["admin"], roles: ["admin"],
seedVersion: 1, seedVersion: 1,
}, },
{
name: "laneCheck",
value: "0",
active: false,
description:
"Allows the driver to scan a lane and see what is in the lane and details about each pallet.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "dockScan",
value: "0",
active: false,
description:
"Enables dock door scanning, must have a dock scanner setup for this to work.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "cycleCounting",
value: "0",
active: false,
description: "Enables a cycle count to be triggered from the scanner.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
{
name: "scannerIds",
value: "10",
active: false,
description:
"How many scanners ids are setup for this, there should be a lst_scanner instance created.",
moduleName: "mobile",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
]; ];
export const baseSettingValidationCheck = async () => { export const baseSettingValidationCheck = async () => {

View File

@@ -1,5 +1,6 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import getServers from "./serverData.route.js"; 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";

View File

@@ -1,14 +1,21 @@
import type { Express } from "express"; import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js"; import { requireAuth } from "../middleware/auth.middleware.js";
import restart from "./tcpRestart.route.js"; import restart from "./tcpRestart.route.js";
import start from "./tcpStart.route.js"; import start from "./tcpStart.route.js";
import stop from "./tcpStop.route.js"; import stop from "./tcpStop.route.js";
export const setupTCPRoutes = (baseUrl: string, app: Express) => { export const setupTCPRoutes = (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/tcp/start`, requireAuth, start); app.use(`${baseUrl}/api/tcp/start`, requireAuth, start);
app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop); app.use(`${baseUrl}/api/tcp/stop`, requireAuth, stop);
app.use(`${baseUrl}/api/tcp/restart`, requireAuth, restart); app.use(
`${baseUrl}/api/tcp/restart`,
requireAuth,
restart,
);
// all other system should be under /api/system/* // all other system should be under /api/system/*
}; };

View File

@@ -0,0 +1,148 @@
import { and, count, countDistinct, gte, lt, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { analytics } from "../db/schema/analytics.schema.js";
import { analyticsDaily } from "../db/schema/dailyAnalytics.schema.js";
export const ignoredRoutePrefixes = [
"/health",
"/favicon.ico",
"/socket.io",
"/lst/api/ws",
"/lst-config.js",
];
export function shouldIgnoreRoute(path: string) {
return ignoredRoutePrefixes.some((prefix) => path.startsWith(prefix));
}
type CreateRouteHitInput = {
method: string;
routePattern: string;
actualPath: string;
statusCode: number;
durationMs: number;
module?: string | null;
userId?: string | null;
userEmail?: string | null;
ipAddress?: string | null;
userAgent?: string | null;
};
export async function createRouteHit(input: CreateRouteHitInput) {
await db.insert(analytics).values(input);
}
function getPreviousBusinessDayWindow(date = new Date()) {
const end = new Date(date);
end.setHours(7, 0, 0, 0);
const start = new Date(end);
start.setDate(start.getDate() - 1);
const businessDate = start.toISOString().slice(0, 10);
return {
start,
end,
businessDate,
};
}
export async function runRouteHitAnalyticsCron(): Promise<void> {
const result = await aggregateRouteHitsForBusinessDay();
await cleanupOldRouteHits();
console.log("Route hit analytics aggregated", result);
}
export async function aggregateRouteHitsForBusinessDay() {
const { start, end, businessDate } = getPreviousBusinessDayWindow();
const rows = await db
.select({
businessDate: sql<string>`CAST(${businessDate} AS date)`,
method: analytics.method,
routePattern: analytics.routePattern,
module: sql<string>`COALESCE(${analytics.module}, 'unknown')`,
totalHits: count(),
uniqueUsers: countDistinct(analytics.userId),
successCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} < 400)
`,
errorCount: sql<number>`
COUNT(*) FILTER (WHERE ${analytics.statusCode} >= 400)
`,
avgDurationMs: sql<number>`
COALESCE(ROUND(AVG(${analytics.durationMs})), 0)
`,
maxDurationMs: sql<number>`
COALESCE(MAX(${analytics.durationMs}), 0)
`,
firstHitAt: sql<Date>`
COALESCE(MIN(${analytics.createdAt}), NOW())
`,
lastHitAt: sql<Date>`
COALESCE(MAX(${analytics.createdAt}), NOW())
`,
})
.from(analytics)
.where(and(gte(analytics.createdAt, start), lt(analytics.createdAt, end)))
.groupBy(
analytics.method,
analytics.routePattern,
sql`COALESCE(${analytics.module}, 'unknown')`,
);
if (rows.length === 0) {
return {
businessDate,
inserted: 0,
};
}
const values = rows.map((row) => ({
...row,
businessDate: row.businessDate,
firstHitAt: new Date(row.firstHitAt),
lastHitAt: new Date(row.lastHitAt),
}));
await db
.insert(analyticsDaily)
.values(values)
.onConflictDoUpdate({
target: [
analyticsDaily.businessDate,
analyticsDaily.method,
analyticsDaily.routePattern,
analyticsDaily.module,
],
set: {
totalHits: sql`excluded.total_hits`,
uniqueUsers: sql`excluded.unique_users`,
successCount: sql`excluded.success_count`,
errorCount: sql`excluded.error_count`,
avgDurationMs: sql`excluded.avg_duration_ms`,
maxDurationMs: sql`excluded.max_duration_ms`,
firstHitAt: sql`excluded.first_hit_at`,
lastHitAt: sql`excluded.last_hit_at`,
updatedAt: sql`now()`,
},
});
return {
businessDate,
inserted: rows.length,
};
}
export async function cleanupOldRouteHits() {
await db
.delete(analytics)
.where(lt(analytics.createdAt, sql`now() - interval '4 days'`));
}

View File

@@ -1,9 +1,12 @@
import { createAccessControl } from "better-auth/plugins/access"; import { createAccessControl } from "better-auth/plugins/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
export const statement = { export const statement = {
...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const; } as const;
@@ -14,13 +17,22 @@ export const user = ac.newRole({
notifications: ["read", "create"], notifications: ["read", "create"],
}); });
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
});
export const admin = ac.newRole({ export const admin = ac.newRole({
app: ["read", "create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"], app: ["read", "create", "share", "update", "delete", "readAll"],
user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"], quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"], notifications: ["read", "create", "share", "update", "delete", "readAll"],
}); });

View File

@@ -13,7 +13,7 @@ import {
//import { eq } from "drizzle-orm"; //import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import * as rawSchema from "../db/schema/auth.schema.js"; import * as rawSchema from "../db/schema/auth.schema.js";
import { ac, admin, systemAdmin, user } from "./auth.permissions.js"; import { ac, admin, manager, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js"; import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js"; import { sendEmail } from "./sendEmail.utils.js";
@@ -163,6 +163,7 @@ export const auth = betterAuth({
roles: { roles: {
admin, admin,
user, user,
manager,
systemAdmin, systemAdmin,
}, },
}), }),

View File

@@ -10,7 +10,7 @@ export async function generateUniquePin() {
const pin = generateSixDigitPin(); const pin = generateSixDigitPin();
const existing = await db.query.scanUser.findFirst({ const existing = await db.query.scanUser.findFirst({
where: (u, { eq }) => eq(u.pinHash, pin), // ⚠️ we'll fix this below where: (u, { eq }) => eq(u.pinHash, pin),
}); });
if (!existing) if (!existing)
@@ -37,3 +37,13 @@ export async function generateUniquePin() {
room: "", room: "",
}); });
} }
// export const pinExists = async (pin: string | number) => {
// const existing = await db.query.scanUser.findFirst({
// where: (u, { eq }) => eq(u.pinHash, pin),
// });
// if (!existing) return true;
// return false;
// };

View File

@@ -0,0 +1,61 @@
import { isUmamiEnabled, umamiConfig } from "../configs/umami.config.js";
type TrackLstEventInput = {
eventName: string;
eventData?: Record<string, unknown>;
url?: string;
hostname?: string;
};
export async function trackLstEvent({
eventName,
eventData,
url = "/backend",
hostname = umamiConfig.server,
}: TrackLstEventInput): Promise<void> {
if (!isUmamiEnabled()) return;
try {
await fetch(`${umamiConfig.umamiHost}/api/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "LST-Backend",
},
body: JSON.stringify({
type: "event",
payload: {
website: umamiConfig.umamiWebsiteId,
name: eventName,
url,
hostname,
language: "en-US",
screen: "backend",
data: {
app: umamiConfig.appName,
site: umamiConfig.site,
server: umamiConfig.server,
appVersion: umamiConfig.appVersion,
source: "backend",
...eventData,
},
},
}),
});
} catch (err) {
console.error("Failed to send Umami backend event", err);
}
}
/*
await trackLstEvent({
eventName: "label_print_completed",
url: "/backend/printers",
eventData: {
module: "printers",
printerName,
labelType,
},
});
*/

View File

@@ -1,4 +1,5 @@
import type { Express } from "express"; import type { Express } from "express";
import getActiveJobs from "./cronerActiveJobs.route.js"; import getActiveJobs from "./cronerActiveJobs.route.js";
import jobStatusChange from "./cronerStatusChange.route.js"; import jobStatusChange from "./cronerStatusChange.route.js";
export const setupUtilsRoutes = (baseUrl: string, app: Express) => { export const setupUtilsRoutes = (baseUrl: string, app: Express) => {

View File

@@ -7,7 +7,16 @@
<title>Logistics Support Tool</title> <title>Logistics Support Tool</title>
</head> </head>
<body> <body>
<script>
const configScript = document.createElement("script");
configScript.src = `${window.location.origin}/lst/api/lst-config.js`;
configScript.defer = false;
document.head.appendChild(configScript);
</script>
<div id="root"></div> <div id="root"></div>
<script defer src="https://stats.tuffraid.net/script.js" data-website-id="49bc2489-3930-4358-a13d-1cc609336572"></script>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -19,11 +19,14 @@ export default function Header() {
const { data: session } = useSession(); const { data: session } = useSession();
const { signOut } = authClient; const { signOut } = authClient;
const router = useRouterState(); const router = useRouterState();
const navigate = useNavigate(); const navigate = useNavigate();
const currentPath = router.location.href; const currentPath = router.location.href;
return ( return (
<header className="sticky top-0 z-50 flex w-full items-center border-b bg-background"> <header
className={`sticky top-0 z-50 flex w-full items-center border-b ${session?.session.impersonatedBy ? "bg-amber-600" : "bg-background"} `}
>
<div className="flex justify-between w-full"> <div className="flex justify-between w-full">
<div className="flex items-center gap-2 px-4"> <div className="flex items-center gap-2 px-4">
<div className="flex flex-row"> <div className="flex flex-row">
@@ -48,6 +51,20 @@ export default function Header() {
<span className="font-semibold text-2xl">Logistics Support Tool</span> <span className="font-semibold text-2xl">Logistics Support Tool</span>
</div> </div>
<div className="m-1 flex gap-1"> <div className="m-1 flex gap-1">
<div>
{session?.session.impersonatedBy && (
<Button
onClick={async () => {
await authClient.admin.stopImpersonating();
await authClient.getSession();
window.location.assign("/lst/app/admin/users");
}}
>
Stop Impersonating
</Button>
)}
</div>
<div> <div>
<ModeToggle /> <ModeToggle />
</div> </div>

View File

@@ -0,0 +1,51 @@
import { useRouter } from "@tanstack/react-router";
import { Card, CardContent, CardHeader } from "./ui/card";
export default function NotFound() {
const router = useRouter();
let url: string;
if (window.location.origin.includes("localhost")) {
url = `https://www.youtube.com/watch?v=dQw4w9WgXcQ`;
} else if (window.location.origin.includes("vms006")) {
url = `https://${window.location.hostname.replace("vms006", "prod.alpla.net/")}lst/app/old`;
} else {
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
}
return (
<div className="flex items-center justify-center bg-background text-foreground">
<Card>
<CardHeader>
<p className="text-2xl">
Oops, Looks like you hit a link you shouldn't have
</p>
</CardHeader>
<CardContent>
<p className="mt-3 text-muted-foreground">
Your have tried to go to a page that you are not authorized to be
at.
</p>
<div className="flex justify-center">
<div>
<a href={`${url}`} target="_blank" rel="noopener">
<b>
<strong>OLD - LST Home</strong>
</b>
</a>
</div>
<div>
<button
type="button"
className="w-64"
onClick={() => router.navigate({ to: "/", replace: true })}
>
<strong>Home</strong>
</button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react"; import { Bell, Logs, Server, Settings, UsersRound } from "lucide-react";
import { getSettings } from "../../lib/queries/getSettings";
import { import {
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
@@ -23,6 +24,7 @@ import {
export default function AdminSidebar({ session }: any) { export default function AdminSidebar({ session }: any) {
const { setOpen } = useSidebar(); const { setOpen } = useSidebar();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const items = [ const items = [
{ {
title: "Notifications", title: "Notifications",
@@ -68,9 +70,11 @@ export default function AdminSidebar({ session }: any) {
title: "Scan users", title: "Scan users",
url: "/admin/scanUsers", url: "/admin/scanUsers",
icon: UsersRound, icon: UsersRound,
role: ["systemAdmin", "admin"], role: ["systemAdmin", "admin", "manager"],
module: "admin", module: "admin",
active: true, active:
!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active,
}, },
]; ];
return ( return (
@@ -79,9 +83,9 @@ export default function AdminSidebar({ session }: any) {
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<> <div key={item.title}>
{item.role.includes(session.user.role) && ( {item.role.includes(session.user.role) && item.active && (
<SidebarMenuItem key={item.title}> <SidebarMenuItem>
<SidebarMenuButton asChild> <SidebarMenuButton asChild>
<Link to={item.url} onClick={() => setOpen(false)}> <Link to={item.url} onClick={() => setOpen(false)}>
<item.icon /> <item.icon />
@@ -90,7 +94,7 @@ export default function AdminSidebar({ session }: any) {
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
)} )}
</> </div>
))} ))}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>

View File

@@ -1,11 +1,12 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { Link, useRouterState } from "@tanstack/react-router"; import { Link, useRouterState } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react"; import { ChevronRight, ScrollText } from "lucide-react";
import { getSettings } from "../../lib/queries/getSettings";
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "../ui/collapsible"; } from "../ui/collapsible";
import { import {
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
@@ -19,43 +20,55 @@ import {
useSidebar, useSidebar,
} from "../ui/sidebar"; } from "../ui/sidebar";
const docs = [
{
title: "Notifications",
url: "/intro",
//icon,
isActive: window.location.pathname.includes("notifications") ?? false,
items: [
{
title: "Reprints",
url: "/reprints",
},
{
title: "New Blocking order",
url: "/qualityBlocking",
},
],
},
{
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();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
const pathname = useRouterState({ const pathname = useRouterState({
select: (s) => s.location.pathname, select: (s) => s.location.pathname,
}); });
const isNotifications = pathname.includes("notifications"); const isNotifications = pathname.includes("notifications");
const docs = [
{
title: "Notifications",
url: "notifications/intro",
//icon,
isActive: true,
items: [
{
title: "Reprints",
icon: ScrollText,
url: "notifications/reprints",
},
{
title: "New Blocking order",
icon: ScrollText,
url: "notifications/qualityBlocking",
},
],
},
{
title: "Mobile",
url: "mobile/updateInstructions",
isActive:
!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active,
items: [
{
title: "Update Instructions",
icon: ScrollText,
url: "mobile/updateInstructions",
},
// {
// title: "Settings",
// icon: ScrollText,
// url: "mobile/mobile-settings",
// },
],
},
];
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Docs</SidebarGroupLabel> <SidebarGroupLabel>Docs</SidebarGroupLabel>
@@ -72,42 +85,44 @@ export default function DocBar() {
</SidebarMenu> </SidebarMenu>
<SidebarMenu> <SidebarMenu>
{docs.map((item) => ( {docs.map((item) => (
<Collapsible <div key={item.title}>
key={item.title} {item.isActive && (
asChild <Collapsible
defaultOpen={isNotifications} asChild
className="group/collapsible" defaultOpen={isNotifications}
> className="group/collapsible"
<SidebarMenuItem> >
<CollapsibleTrigger asChild> <SidebarMenuItem>
<SidebarMenuButton tooltip={item.title}> <CollapsibleTrigger asChild>
<Link <SidebarMenuButton tooltip={item.title}>
to={"/docs/$"} <Link to={"/docs/$"} params={{ _splat: `${item.url}` }}>
params={{ _splat: `notifications${item.url}` }} {item.title}
> </Link>
{item.title} <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</Link> </SidebarMenuButton>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> </CollapsibleTrigger>
</SidebarMenuButton> <CollapsibleContent>
</CollapsibleTrigger> <SidebarMenuSub>
<CollapsibleContent> {item.items?.map((subItem) => (
<SidebarMenuSub> <SidebarMenuSubItem key={subItem.title}>
{item.items?.map((subItem) => ( <SidebarMenuSubButton asChild>
<SidebarMenuSubItem key={subItem.title}> <Link
<SidebarMenuSubButton asChild> to={"/docs/$"}
<Link params={{ _splat: `${subItem.url}` }}
to={"/docs/$"} onClick={() => setOpen(false)}
params={{ _splat: `notifications${subItem.url}` }} >
> <subItem.icon />
{subItem.title} <span>{subItem.title}</span>
</Link> </Link>
</SidebarMenuSubButton> </SidebarMenuSubButton>
</SidebarMenuSubItem> </SidebarMenuSubItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
</CollapsibleContent> </CollapsibleContent>
</SidebarMenuItem> </SidebarMenuItem>
</Collapsible> </Collapsible>
)}
</div>
))} ))}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { ScanText, ScrollText } from "lucide-react"; import { ScanText } from "lucide-react";
import { import {
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
@@ -10,14 +10,14 @@ import {
useSidebar, useSidebar,
} from "../ui/sidebar"; } from "../ui/sidebar";
export default function MobileBar({ session }: any) { export default function MobileBar() {
const { setOpen } = useSidebar(); const { setOpen } = useSidebar();
const items = [ const items = [
{ // {
title: "Update Instructions", // title: "Update Instructions",
url: "/", // url: "/",
icon: ScrollText, // icon: ScrollText,
}, // },
{ {
title: "Scan Log", title: "Scan Log",
url: "/", url: "/",
@@ -25,8 +25,6 @@ export default function MobileBar({ session }: any) {
}, },
]; ];
console.log(session);
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Mobile</SidebarGroupLabel> <SidebarGroupLabel>Mobile</SidebarGroupLabel>

View File

@@ -1,3 +1,4 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -6,12 +7,14 @@ import {
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import AdminSidebar from "./AdminBar"; import AdminSidebar from "./AdminBar";
import DocBar from "./DocBar"; import DocBar from "./DocBar";
import MobileBar from "./MobileBar"; import MobileBar from "./MobileBar";
export function AppSidebar() { export function AppSidebar() {
const { data: session } = useSession(); const { data: session } = useSession();
const { data: settings, isLoading } = useSuspenseQuery(getSettings());
return ( return (
<Sidebar <Sidebar
@@ -24,10 +27,15 @@ export function AppSidebar() {
<SidebarMenuItem> <SidebarMenuItem>
<SidebarContent> <SidebarContent>
<DocBar /> <DocBar />
<MobileBar session={session} /> {!isLoading &&
settings.filter((n: any) => n.name === "mobile")[0].active && (
<MobileBar />
)}
{session && {session &&
(session.user.role === "admin" || (session.user.role === "admin" ||
session.user.role === "systemAdmin") && ( session.user.role === "systemAdmin" ||
session.user.role === "manager") && (
<AdminSidebar session={session} /> <AdminSidebar session={session} />
)} )}
</SidebarContent> </SidebarContent>

View File

@@ -1,64 +1,67 @@
import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"
import { Slot } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"
import type * as React from "react"; import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
destructive: outline:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
outline: secondary:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
secondary: ghost:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
ghost: destructive:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default:
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
icon: "size-9", lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", icon: "size-8",
"icon-sm": "size-8", "icon-xs":
"icon-lg": "size-10", "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
}, "icon-sm":
}, "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
defaultVariants: { "icon-lg": "size-9",
variant: "default", },
size: "default", },
}, defaultVariants: {
}, variant: "default",
); size: "default",
},
}
)
function Button({ function Button({
className, className,
variant = "default", variant = "default",
size = "default", size = "default",
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean
}) { }) {
const Comp = asChild ? Slot.Root : "button"; const Comp = asChild ? Slot.Root : "button"
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); )
} }
export { Button, buttonVariants }; export { Button, buttonVariants }

View File

@@ -0,0 +1,166 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -1,5 +1,4 @@
import { Tooltip as TooltipPrimitive } from "radix-ui"; import { Tooltip as TooltipPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -40,7 +39,7 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}
@@ -52,4 +51,4 @@ function TooltipContent({
); );
} }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

@@ -0,0 +1,137 @@
import { useMutation } from "@tanstack/react-query";
import { Button } from "../../components/ui/button";
import { Separator } from "../../components/ui/separator";
export default function UpdateInstructions() {
const getFile = useMutation({
mutationFn: async () => {
// 1. Fetch the file from the public folder
const response = await fetch(
`/lst/app/stage-now/${window.LST_CONFIG?.server}-stageNow.pdf`,
);
if (!response.ok) throw new Error("Network response was not ok");
// 2. Convert to blob
return await response.blob();
},
onSuccess: (blob) => {
// 3. Create a temporary anchor element to trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${window.LST_CONFIG?.server}-stageNow.pdf`; // Desired filename
document.body.appendChild(a);
a.click();
// 4. Cleanup
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
});
return (
<div className="flex flex-row gap-2">
<div className="w-1/2">
<div className="flex flex-col gap-1 justify-center">
<div>
<p className="text-center text-3xl">
Updating the lst mobile scanner app
</p>
<p className="text-center text-sm">
NOTE: LST Mobile only works on TC8300
</p>
</div>
<div className="flex justify-center">
<Button
onClick={() => getFile.mutate()}
disabled={getFile.isPending}
>
{getFile.isPending ? "Downloading..." : "Get StageNow Codes"}
</Button>
</div>
</div>
<Separator className="m-3" />
<div>
<p className="text-2xl text-center">
How to know the scanner has an update?
</p>
<p>
The bottom part of the scanner will show a red or orange bar
indicating there is an update. As shown below
</p>
<div className="flex flex-row gap-2 justify-center">
<div className="w-1/2">
<img
src="/lst/app/imgs/docs/mobile/critical_update.png"
alt="Home"
className="max-w-[50%] h-auto"
/>
</div>
<div className="w-1/2">
<img
src="/lst/app/imgs/docs/mobile/update.png"
alt="Home"
className="max-w-[50%] h-auto"
/>
</div>
</div>
</div>
<Separator className="m-3" />
<div>
<p className="text-2xl text-center">
To update the scanner follow the below steps.
</p>
<p>Step 1) Tap the 3 dots top right of the home screen</p>
<img
src="/lst/app/imgs/docs/mobile/ehs_homeScreen.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>Step 2) Tap tools</p>
<img
src="/lst/app/imgs/docs/mobile/ehs_menu.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>Step 3) Tap Stage Now</p>
<img
src="/lst/app/imgs/docs/mobile/tools.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
<p>
Step 4) Scan the 3 barcode's to the right or from the printed sheet
</p>
<img
src="/lst/app/imgs/docs/mobile/stagenow.png"
alt="Home"
className="max-w-[25%] h-auto m-3"
/>
</div>
</div>
<div className="w-1/2">
<p>Scan Commands</p>
<Separator className="m-3" />
<div className="flex flex-col justify-center">
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-1.png`}
alt="Home"
className="m-3"
/>
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-2.png`}
alt="Home"
className="m-3"
/>
<img
src={`/lst/app/imgs/docs/mobile/${window.LST_CONFIG?.server}-3.png`}
alt="Home"
className="m-3"
/>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,47 @@
import type { Router } from "@tanstack/react-router";
import axios from "axios";
import { toast } from "sonner";
let appRouter: Router<any, any> | null = null;
export function setApiRouter(router: Router<any, any>) {
appRouter = router;
}
export const api = axios.create({
baseURL: "/lst/api",
withCredentials: true,
timeout: 15000,
});
api.interceptors.response.use(
(response) => response,
(error) => {
const isNetworkError =
error.code === "ERR_NETWORK" ||
error.code === "ECONNABORTED" ||
error.message === "Network Error" ||
error.message === "Failed to fetch" ||
!error.response;
if (error.response?.status === 403) {
// redirect, toast, or show forbidden page
toast.error("Unauthorized to be here");
appRouter?.navigate({ to: "/forbidden", replace: true });
}
if (error.response?.status === 401) {
// redirect, toast, or show forbidden page
toast.error("Unauthorized to be here");
appRouter?.navigate({ to: "/login", replace: true });
}
if (isNetworkError) {
appRouter?.navigate({ to: "/app-down", replace: true });
}
return Promise.reject(error);
},
);

View File

@@ -1,6 +1,12 @@
import { adminClient, genericOAuthClient } from "better-auth/client/plugins"; import { redirect } from "@tanstack/react-router";
import {
adminClient,
genericOAuthClient,
usernameClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions"; import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`, baseURL: `${window.location.origin}/lst/api/auth`,
@@ -10,11 +16,21 @@ export const authClient = createAuthClient({
roles: { roles: {
admin, admin,
user, user,
manager,
systemAdmin, systemAdmin,
}, },
}), }),
genericOAuthClient(), genericOAuthClient(),
usernameClient(),
], ],
fetchOptions: {
onError() {
redirect({
to: "/app-down",
replace: true,
});
},
},
}); });
export const { useSession, signUp, signIn, signOut } = authClient; export const { useSession, signUp, signIn, signOut } = authClient;

View File

@@ -1,21 +1,71 @@
import { createAccessControl } from "better-auth/plugins/access"; import { createAccessControl } from "better-auth/plugins/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
/*
When new perms are added based on there criteria make sure they are added here as well
*/
type SelectableRole = {
label: string;
value: string;
};
export const selectableRoles: SelectableRole[] = [
{ label: "User", value: "user" },
{ label: "Manager", value: "manager" },
{ label: "Admin", value: "admin" },
{ label: "System Admin", value: "systemAdmin" },
];
export const statement = { export const statement = {
project: ["create", "share", "update", "delete"], ...defaultStatements,
user: ["ban"], app: ["read", "create", "share", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
} as const; } as const;
export const ac = createAccessControl(statement); export const ac = createAccessControl(statement);
export const user = ac.newRole({ export const user = ac.newRole({
project: ["create"], app: ["read", "create"],
notifications: ["read", "create"],
});
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
}); });
export const admin = ac.newRole({ export const admin = ac.newRole({
project: ["create", "update"], app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
}); });
export const systemAdmin = ac.newRole({ export const systemAdmin = ac.newRole({
project: ["create", "update", "delete"], ...adminAc.statements,
user: ["ban"], app: ["read", "create", "share", "update", "delete", "readAll"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
}); });
/* example usage
const canCreateProject = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
},
});
// You can also check multiple resource permissions at the same time
const canCreateProjectAndCreateSale = await authClient.admin.hasPermission({
permissions: {
project: ["create"],
sale: ["create"]
},
});
*/

View File

@@ -13,9 +13,7 @@ const docsMap: Record<string, ComponentType> = {};
for (const path in modules) { for (const path in modules) {
const mod = modules[path] as DocModule; const mod = modules[path] as DocModule;
const slug = path const slug = path.replace("../docs/", "").replace(".tsx", "");
.replace("../docs/", "")
.replace(".tsx", "");
// "notifications/intro" // "notifications/intro"
docsMap[slug] = mod.default; docsMap[slug] = mod.default;
@@ -23,4 +21,4 @@ for (const path in modules) {
export function getDoc(slug: string) { export function getDoc(slug: string) {
return docsMap[slug]; return docsMap[slug];
} }

View File

@@ -0,0 +1,25 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getScanUsers() {
return queryOptions({
queryKey: ["getScanUsers"],
queryFn: () => dataFetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const dataFetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/mobile/auth/user");
if (!data.success) {
throw new Error(data.message ?? "Failed to load scan users");
}
return data.data ?? [];
};

View File

@@ -0,0 +1,23 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { api } from "../apiHelper";
export function getScannerIds() {
return queryOptions({
queryKey: ["getScannerIds"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data } = await api.get("/mobile/available");
return data.data;
};

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function getSettings() { export function getSettings() {
return queryOptions({ return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
} }
const { data } = await axios.get("/lst/api/settings"); const { data } = await api.get("/settings");
return data.data; return data.data;
}; };

View File

@@ -0,0 +1,40 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { authClient } from "../auth-client";
export function getUsers() {
return queryOptions({
queryKey: ["getUsers"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 1500));
}
const { data, error } = await authClient.admin.listUsers({
query: {
// searchValue: "some name",
// searchField: "name",
// searchOperator: "contains",
limit: 100,
offset: 0,
sortBy: "name",
// sortDirection: "desc",
// filterField: "email",
// filterValue: "hello@example.com",
// filterOperator: "eq",
},
});
if (error) {
return error;
}
return data.users;
};

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function notificationSubs(userId?: string) { export function notificationSubs(userId?: string) {
return queryOptions({ return queryOptions({
@@ -16,8 +17,8 @@ const fetch = async (userId?: string) => {
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
} }
const { data } = await axios.get( const { data } = await api.get(
`/lst/api/notification/sub${userId ? `?userId=${userId}` : ""}`, `/notification/sub${userId ? `?userId=${userId}` : ""}`,
); );
return data.data; return data.data;

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function notifications() { export function notifications() {
return queryOptions({ return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
} }
const { data } = await axios.get("/lst/api/notification"); const { data } = await api.get("/notification");
return data.data; return data.data;
}; };

View File

@@ -0,0 +1,16 @@
import { queryOptions } from "@tanstack/react-query";
import { authClient } from "@/lib/auth-client";
export function permissionQuery(permissions: Record<string, string[]>) {
return queryOptions({
queryKey: ["permission", permissions],
queryFn: async () => {
const result = await authClient.admin.hasPermission({
permissions,
});
return result.data?.success ?? false;
},
staleTime: 30_000,
});
}

View File

@@ -1,5 +1,6 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
import { api } from "../apiHelper";
export function servers() { export function servers() {
return queryOptions({ return queryOptions({
@@ -16,7 +17,7 @@ const fetch = async () => {
await new Promise((res) => setTimeout(res, 1500)); await new Promise((res) => setTimeout(res, 1500));
} }
const { data } = await axios.get("/lst/api/servers"); const { data } = await api.get("/servers");
return data.data; return data.data;
}; };

View File

@@ -17,11 +17,13 @@ export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
{Array.from({ length: columns }).map((_, i) => ( <TableRow>
<TableHead key={i}> {Array.from({ length: columns }).map((_, i) => (
<Skeleton className="h-4 w-[80px]" /> <TableHead key={i}>
</TableHead> <Skeleton className="h-4 w-[80px]" />
))} </TableHead>
))}
</TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{Array.from({ length: rows }).map((_, r) => ( {Array.from({ length: rows }).map((_, r) => (

View File

@@ -0,0 +1,65 @@
type RuntimeConfig = {
appName: string;
site: string;
server: string;
appVersion: string;
umamiHost: string;
umamiWebsiteId: string;
};
declare global {
interface Window {
LST_CONFIG?: RuntimeConfig;
umami?: {
track: (eventName: string, eventData?: Record<string, unknown>) => void;
};
}
}
export const runtimeConfig: RuntimeConfig = {
appName: window.LST_CONFIG?.appName ?? "LST",
site: window.LST_CONFIG?.site ?? "unknown",
server: window.LST_CONFIG?.server ?? "unknown",
appVersion: window.LST_CONFIG?.appVersion ?? "dev",
umamiHost: window.LST_CONFIG?.umamiHost ?? "",
umamiWebsiteId: window.LST_CONFIG?.umamiWebsiteId ?? "",
};
export function loadUmami() {
if (!runtimeConfig.umamiHost) return;
if (!runtimeConfig.umamiWebsiteId) return;
if (document.querySelector("script[data-website-id]")) return;
const script = document.createElement("script");
script.defer = true;
script.src = `${runtimeConfig.umamiHost}/script.js`;
script.setAttribute("data-website-id", runtimeConfig.umamiWebsiteId);
document.head.appendChild(script);
}
export function trackLstEvent(
eventName: string,
eventData?: Record<string, unknown>,
) {
window.umami?.track(eventName, {
app: runtimeConfig.appName,
site: runtimeConfig.site,
server: runtimeConfig.server,
appVersion: runtimeConfig.appVersion,
...eventData,
});
}
/*
event type
trackLstEvent("exampleClick", {
module: "example",
action: "test_click",
label: "Example Button",
page: window.location.pathname,
});
*/

View File

@@ -3,7 +3,10 @@ import { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css"; import "./index.css";
import { createRouter, RouterProvider } from "@tanstack/react-router"; import { createRouter, RouterProvider } from "@tanstack/react-router";
import NotFound from "./components/NotFound";
import { setApiRouter } from "./lib/apiHelper";
import socket from "./lib/socket.io"; import socket from "./lib/socket.io";
import { loadUmami } from "./lib/umami.utils";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
@@ -12,8 +15,9 @@ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
retry: 0, retry: 2,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000),
}, },
}, },
}); });
@@ -26,8 +30,11 @@ const router = createRouter({
context: { context: {
queryClient, queryClient,
}, },
defaultNotFoundComponent: NotFound,
}); });
setApiRouter(router);
// Register the router instance for type safety // Register the router instance for type safety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
@@ -38,6 +45,7 @@ declare module "@tanstack/react-router" {
// Render the app // Render the app
const rootElement = document.getElementById("root")!; const rootElement = document.getElementById("root")!;
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
loadUmami();
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<StrictMode> <StrictMode>

View File

@@ -9,10 +9,13 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as ForbiddenRouteImport } from './routes/forbidden'
import { Route as AppDownRouteImport } from './routes/app-down'
import { Route as AboutRouteImport } from './routes/about' import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index' 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 AdminUsersRouteImport } from './routes/admin/users'
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 AdminScanUsersRouteImport } from './routes/admin/scanUsers'
@@ -23,6 +26,16 @@ import { Route as authUserSignupRouteImport } from './routes/(auth)/user.signup'
import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword' import { Route as authUserResetpasswordRouteImport } from './routes/(auth)/user.resetpassword'
import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile' import { Route as authUserProfileRouteImport } from './routes/(auth)/user.profile'
const ForbiddenRoute = ForbiddenRouteImport.update({
id: '/forbidden',
path: '/forbidden',
getParentRoute: () => rootRouteImport,
} as any)
const AppDownRoute = AppDownRouteImport.update({
id: '/app-down',
path: '/app-down',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({ const AboutRoute = AboutRouteImport.update({
id: '/about', id: '/about',
path: '/about', path: '/about',
@@ -43,6 +56,11 @@ const DocsSplatRoute = DocsSplatRouteImport.update({
path: '/docs/$', path: '/docs/$',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/admin/users',
path: '/admin/users',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({ const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin/settings', id: '/admin/settings',
path: '/admin/settings', path: '/admin/settings',
@@ -92,12 +110,15 @@ const authUserProfileRoute = authUserProfileRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/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/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
@@ -107,12 +128,15 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/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/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs': typeof DocsIndexRoute '/docs': typeof DocsIndexRoute
'/user/profile': typeof authUserProfileRoute '/user/profile': typeof authUserProfileRoute
@@ -123,12 +147,15 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/about': typeof AboutRoute '/about': typeof AboutRoute
'/app-down': typeof AppDownRoute
'/forbidden': typeof ForbiddenRoute
'/(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/scanUsers': typeof AdminScanUsersRoute
'/admin/servers': typeof AdminServersRoute '/admin/servers': typeof AdminServersRoute
'/admin/settings': typeof AdminSettingsRoute '/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/docs/$': typeof DocsSplatRoute '/docs/$': typeof DocsSplatRoute
'/docs/': typeof DocsIndexRoute '/docs/': typeof DocsIndexRoute
'/(auth)/user/profile': typeof authUserProfileRoute '/(auth)/user/profile': typeof authUserProfileRoute
@@ -140,12 +167,15 @@ export interface FileRouteTypes {
fullPaths: fullPaths:
| '/' | '/'
| '/about' | '/about'
| '/app-down'
| '/forbidden'
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
| '/user/profile' | '/user/profile'
@@ -155,12 +185,15 @@ export interface FileRouteTypes {
to: to:
| '/' | '/'
| '/about' | '/about'
| '/app-down'
| '/forbidden'
| '/login' | '/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs' | '/docs'
| '/user/profile' | '/user/profile'
@@ -170,12 +203,15 @@ export interface FileRouteTypes {
| '__root__' | '__root__'
| '/' | '/'
| '/about' | '/about'
| '/app-down'
| '/forbidden'
| '/(auth)/login' | '/(auth)/login'
| '/admin/logs' | '/admin/logs'
| '/admin/notifications' | '/admin/notifications'
| '/admin/scanUsers' | '/admin/scanUsers'
| '/admin/servers' | '/admin/servers'
| '/admin/settings' | '/admin/settings'
| '/admin/users'
| '/docs/$' | '/docs/$'
| '/docs/' | '/docs/'
| '/(auth)/user/profile' | '/(auth)/user/profile'
@@ -186,12 +222,15 @@ export interface FileRouteTypes {
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute AboutRoute: typeof AboutRoute
AppDownRoute: typeof AppDownRoute
ForbiddenRoute: typeof ForbiddenRoute
authLoginRoute: typeof authLoginRoute authLoginRoute: typeof authLoginRoute
AdminLogsRoute: typeof AdminLogsRoute AdminLogsRoute: typeof AdminLogsRoute
AdminNotificationsRoute: typeof AdminNotificationsRoute AdminNotificationsRoute: typeof AdminNotificationsRoute
AdminScanUsersRoute: typeof AdminScanUsersRoute AdminScanUsersRoute: typeof AdminScanUsersRoute
AdminServersRoute: typeof AdminServersRoute AdminServersRoute: typeof AdminServersRoute
AdminSettingsRoute: typeof AdminSettingsRoute AdminSettingsRoute: typeof AdminSettingsRoute
AdminUsersRoute: typeof AdminUsersRoute
DocsSplatRoute: typeof DocsSplatRoute DocsSplatRoute: typeof DocsSplatRoute
DocsIndexRoute: typeof DocsIndexRoute DocsIndexRoute: typeof DocsIndexRoute
authUserProfileRoute: typeof authUserProfileRoute authUserProfileRoute: typeof authUserProfileRoute
@@ -201,6 +240,20 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/forbidden': {
id: '/forbidden'
path: '/forbidden'
fullPath: '/forbidden'
preLoaderRoute: typeof ForbiddenRouteImport
parentRoute: typeof rootRouteImport
}
'/app-down': {
id: '/app-down'
path: '/app-down'
fullPath: '/app-down'
preLoaderRoute: typeof AppDownRouteImport
parentRoute: typeof rootRouteImport
}
'/about': { '/about': {
id: '/about' id: '/about'
path: '/about' path: '/about'
@@ -229,6 +282,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsSplatRouteImport preLoaderRoute: typeof DocsSplatRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/users': {
id: '/admin/users'
path: '/admin/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AdminUsersRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/settings': { '/admin/settings': {
id: '/admin/settings' id: '/admin/settings'
path: '/admin/settings' path: '/admin/settings'
@@ -298,12 +358,15 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute, AboutRoute: AboutRoute,
AppDownRoute: AppDownRoute,
ForbiddenRoute: ForbiddenRoute,
authLoginRoute: authLoginRoute, authLoginRoute: authLoginRoute,
AdminLogsRoute: AdminLogsRoute, AdminLogsRoute: AdminLogsRoute,
AdminNotificationsRoute: AdminNotificationsRoute, AdminNotificationsRoute: AdminNotificationsRoute,
AdminScanUsersRoute: AdminScanUsersRoute, AdminScanUsersRoute: AdminScanUsersRoute,
AdminServersRoute: AdminServersRoute, AdminServersRoute: AdminServersRoute,
AdminSettingsRoute: AdminSettingsRoute, AdminSettingsRoute: AdminSettingsRoute,
AdminUsersRoute: AdminUsersRoute,
DocsSplatRoute: DocsSplatRoute, DocsSplatRoute: DocsSplatRoute,
DocsIndexRoute: DocsIndexRoute, DocsIndexRoute: DocsIndexRoute,
authUserProfileRoute: authUserProfileRoute, authUserProfileRoute: authUserProfileRoute,

View File

@@ -29,30 +29,43 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
email: loginEmail, login: loginEmail,
password: "", password: "",
rememberMe: rememberMe, rememberMe: rememberMe,
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
// set remember me incase we want it later // set remember me incase we want it later
const loginValue = value.login.trim();
const isEmailLogin = loginValue.includes("@");
if (value.rememberMe) { if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString()); localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email.toLocaleLowerCase()); localStorage.setItem("loginEmail", loginValue.toLocaleLowerCase());
} else { } else {
localStorage.removeItem("rememberMe"); localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail"); localStorage.removeItem("loginEmail");
} }
try { try {
const login = await authClient.signIn.email({ const login = isEmailLogin
email: value.email, ? await authClient.signIn.email({
password: value.password, email: loginValue.toLowerCase(),
fetchOptions: { password: value.password,
onSuccess: () => { fetchOptions: {
navigate({ to: redirectPath ?? "/" }); onSuccess: () => {
}, navigate({ to: redirectPath ?? "/" });
}, },
}); },
})
: await authClient.signIn.username({
username: loginValue,
password: value.password,
fetchOptions: {
onSuccess: () => {
navigate({ to: redirectPath ?? "/" });
},
},
});
if (login.error) { if (login.error) {
toast.error(`${login.error?.message}`); toast.error(`${login.error?.message}`);
@@ -95,11 +108,11 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
form.handleSubmit(); form.handleSubmit();
}} }}
> >
<form.AppField name="email"> <form.AppField name="login">
{(field) => ( {(field) => (
<field.InputField <field.InputField
label="Email" label="Username or Email Address"
inputType="email" inputType="text"
required={rememberMe} required={rememberMe}
/> />
)} )}

View File

@@ -45,10 +45,12 @@ export default function NotificationsSubCard({ user }: any) {
let n: any = []; let n: any = [];
if (data) { if (data) {
n = data.map((i: any) => ({ n = data
label: i.name, .filter((n: any) => n.active)
value: i.id, .map((i: any) => ({
})); label: i.name,
value: i.id,
}));
} }
return ( return (

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { authClient, useSession } from "@/lib/auth-client"; import { authClient, useSession } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff"; import { useAppForm } from "@/lib/formSutff";
import { Spinner } from "../../components/ui/spinner"; import { Spinner } from "../../components/ui/spinner";
import ChangePassword from "./-components/ChangePassword"; import ChangePassword from "./-components/ChangePassword";
import NotificationsSubCard from "./-components/NotificationsSubCard"; import NotificationsSubCard from "./-components/NotificationsSubCard";
@@ -37,6 +38,7 @@ export const Route = createFileRoute("/(auth)/user/profile")({
function RouteComponent() { function RouteComponent() {
const { data: session } = useSession(); const { data: session } = useSession();
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
name: session?.user.name, name: session?.user.name,

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff"; import { useAppForm } from "@/lib/formSutff";
import { Separator } from "../../components/ui/separator";
export const Route = createFileRoute("/(auth)/user/signup")({ export const Route = createFileRoute("/(auth)/user/signup")({
component: RouteComponent, component: RouteComponent,
@@ -22,6 +23,7 @@ function RouteComponent() {
email: "", email: "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
username: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
if (value.password !== value.confirmPassword) { if (value.password !== value.confirmPassword) {
@@ -33,6 +35,7 @@ function RouteComponent() {
name: value.name, name: value.name,
email: value.email, email: value.email,
password: value.password, password: value.password,
username: value.username ?? value.name,
callbackURL: `${window.location.origin}/lst/app`, callbackURL: `${window.location.origin}/lst/app`,
}); });
@@ -71,6 +74,15 @@ function RouteComponent() {
/> />
)} )}
</form.AppField> </form.AppField>
<div className="m-2">
<p>Username is option if left blank it will be your name</p>
</div>
<Separator />
<form.AppField name="username">
{(field) => (
<field.InputField label="Username" inputType="text" />
)}
</form.AppField>
{/* Email */} {/* Email */}
<form.AppField name="email"> <form.AppField name="email">

View File

@@ -5,6 +5,7 @@ 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 { TooltipProvider } from "../components/ui/tooltip";
import { useSession } from "../lib/auth-client"; import { useSession } from "../lib/auth-client";
const RootLayout = () => { const RootLayout = () => {
@@ -14,16 +15,17 @@ const RootLayout = () => {
<ThemeProvider> <ThemeProvider>
<SidebarProvider className="flex flex-col" defaultOpen={false}> <SidebarProvider className="flex flex-col" defaultOpen={false}>
<Header /> <Header />
<TooltipProvider>
<div className="relative min-h-[calc(100svh-var(--header-height))]">
<AppSidebar />
<div className="relative min-h-[calc(100svh-var(--header-height))]"> <main className="w-full p-4">
<AppSidebar /> <div className="mx-auto w-full max-w-7xl">
<Outlet />
<main className="w-full p-4"> </div>
<div className="mx-auto w-full max-w-7xl"> </main>
<Outlet /> </div>
</div> </TooltipProvider>
</main>
</div>
<Toaster expand richColors closeButton /> <Toaster expand richColors closeButton />
</SidebarProvider> </SidebarProvider>

View File

@@ -0,0 +1,162 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import axios from "axios";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { api } from "../../../lib/apiHelper";
import { useAppForm } from "../../../lib/formSutff";
import { getScannerIds } from "../../../lib/queries/getScannerIds";
export default function NewScanUser({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const { data, refetch: scannerFetch } = useSuspenseQuery(getScannerIds());
const form = useAppForm({
defaultValues: {
name: "",
scannerId: "",
pinNumber: "",
},
onSubmit: async ({ value }) => {
if (value.scannerId === "") {
toast.error(
"Scanner id is required please select a scanner id before submitting ",
);
return;
}
try {
const { data } = await api.post(
"/lst/api/mobile/auth/user",
{
name: value.name,
pinNumber: value.pinNumber,
scannerId: value.scannerId,
},
{
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
},
);
if (data.success) {
toast.success(
`${value.name}, was just created and can now log into the scanner with PIN: ${value.pinNumber}`,
);
form.reset();
setOpen(false);
refetch();
}
if (!data.success) {
toast.error(data.message);
return;
}
} catch (error) {
console.error(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
scannerFetch();
}
};
const openForm = () => {
setOpen(true);
scannerFetch();
};
let n: any = [];
if (data) {
n = data.map((i: any) => ({
label: i.label,
value: i.value.toString(),
}));
}
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create new user</Button>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Create New Scan user.</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="mb-2">
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div className="w-32">
<form.AppField name="scannerId">
{(field) => (
<field.SelectField
label="Scanner Id"
placeholder="Select New scanner Id"
options={n}
/>
)}
</form.AppField>
</div>
<div className="flex flex-row">
<div>
<form.AppField name="pinNumber">
{(field) => (
<field.InputField
label="Pin Number"
inputType="number"
required={true}
/>
)}
</form.AppField>
</div>
<div className="mt-9 ml-2">
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
form.setFieldValue("pinNumber", data.data[0].pin);
}}
>
New Pin
</Button>
</div>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,153 @@
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { authClient } from "../../../lib/auth-client";
import { selectableRoles } from "../../../lib/auth-permissions";
import { useAppForm } from "../../../lib/formSutff";
export default function NewUser({ refetch }: { refetch: any }) {
const [open, setOpen] = useState(false);
const form = useAppForm({
defaultValues: {
name: "",
email: "",
password: "",
role: "",
username: "",
},
onSubmit: async ({ value }) => {
if (value.name === "" || value.email === "" || value.password === "") {
toast.error("Missing Mandatory data please try again ");
return;
}
try {
const { data, error } = await authClient.admin.createUser({
email: value.email, // required
password: value.password, // required
name: value.name, // required
role: (value.role ?? "user") as any,
data: { username: value.username },
});
if (data?.user) {
toast.success(`${value.name}, was just created `);
form.reset();
setOpen(false);
refetch();
}
if (error) {
toast.error(error.message);
return;
}
} catch (error) {
console.error(error);
}
},
});
const closeModel = (e: boolean) => {
setOpen(e);
if (!e) {
form.reset();
}
};
const openForm = () => {
setOpen(true);
};
return (
<Dialog onOpenChange={(e) => closeModel(e)} open={open}>
<Button onClick={openForm}>Create new user</Button>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Create New Scan user.</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<div className="mb-2">
<form.AppField name="name">
{(field) => (
<field.InputField
label="Name"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div>
<p>
Username can be your windows or anything, if you do not fill this
out your name is used as your username
</p>
</div>
<div className="mb-2">
<form.AppField name="username">
{(field) => (
<field.InputField label="Username" inputType="text" />
)}
</form.AppField>
</div>
<div className="mb-2">
<form.AppField name="email">
{(field) => (
<field.InputField
label="Email"
inputType="email"
required={true}
/>
)}
</form.AppField>
</div>
<div className="mb-2">
<form.AppField name="password">
{(field) => (
<field.InputField
label="Password"
inputType="text"
required={true}
/>
)}
</form.AppField>
</div>
<div className="w-32">
<form.AppField name="role">
{(field) => (
<field.SelectField
label="Roles"
placeholder="Select role"
options={selectableRoles}
/>
)}
</form.AppField>
</div>
<div className="flex justify-end mt-2 ">
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</div>
</form>
</DialogContent>
</Dialog>
);
}

Some files were not shown because too many files have changed in this diff Show More