11 Commits

Author SHA1 Message Date
1f6637c512 fix(build): crashes when files changed :(
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m34s
BREAKING CHANGE: gives a rabbit hole error

closes #24
2026-05-21 21:51:21 -05:00
1840ac5e58 feat(opendock): scheduing updates
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m39s
ref #23
2026-05-21 21:42:18 -05:00
636daaed0a fix(sql queries): disable job would error so now we will check if it exists before trying to kill it
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m37s
2026-05-20 20:49:54 -05:00
71c83062cb ci(docker): changes to the ignore file 2026-05-20 20:49:21 -05:00
cd67c4de80 refactor(opendock): changes to how we do the intergration scheduling
ref #23
2026-05-20 20:49:00 -05:00
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
44 changed files with 8464 additions and 171 deletions

View File

@@ -9,4 +9,4 @@ builds
testFiles
nssm.exe
postgresql-17.9-2-windows-x64.exe
VSCodeUserSetup-x64-1.112.0.msi
VSCodeSetup-x64-1.120.0.msi

View File

@@ -1,5 +1,24 @@
# 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)

View File

@@ -1,7 +1,8 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as opendockAVCheck from "./schema/opendock_articleSetup.js";
import * as scanUserSchema from "./schema/scanUsers.js";
import * as settingsSchema from "./schema/settings.schema.js";
const dbURL = `postgres://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DB}`;
@@ -20,5 +21,7 @@ const queryClient = postgres(dbURL, {
export const db = drizzle(queryClient, {
schema: {
...scanUserSchema,
...settingsSchema,
...opendockAVCheck,
},
});

View File

@@ -14,14 +14,13 @@ export const opendockApt = pgTable(
"opendock_apt",
{
id: uuid("id").defaultRandom().primaryKey(),
release: integer("release").notNull().unique(),
release: integer("release").notNull().unique("opendock_apt_release_unique"),
openDockAptId: text("open_dock_apt_id").notNull(),
appointment: jsonb("appointment").notNull().default([]),
upd_date: timestamp("upd_date").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
},
(table) => ({
releaseIdx: index("opendock_apt_release_idx").on(table.release),
openDockAptIdIdx: index("opendock_apt_opendock_id_idx").on(
table.openDockAptId,
),

View File

@@ -0,0 +1,46 @@
import {
integer,
pgEnum,
pgTable,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod";
export const loadTypeEnum = pgEnum("load_type", ["drop", "live"]);
export const opendockArticleSetup = pgTable(
"opendock_article_setup",
{
id: uuid("id").defaultRandom().primaryKey(),
av: integer("av").notNull(),
description: text("description").notNull(),
customer: text("customer").notNull(), // customer should be a concat of the ID - Desc
customerDescription: text("customer_description").notNull(),
loadType: loadTypeEnum("load_type").notNull().default("drop"),
dock: text("dock").notNull(),
upd_date: timestamp("upd_date").notNull().defaultNow(),
upd_user: text("upd_user").notNull().default("lst-system"),
createdAt: timestamp("created_at").notNull().defaultNow(),
add_user: text("add_user").notNull().default("lst-system"),
},
(table) => ({
uniqueAvCustomer: unique("uq_opendock_article_setup_av_customer").on(
table.av,
table.customer,
),
}),
);
export const opendockArticleSetupSchema =
createSelectSchema(opendockArticleSetup);
export const newOpendockArticleSetupSchema =
createInsertSchema(opendockArticleSetup);
export type OpendockArticleSetup = z.infer<typeof opendockArticleSetupSchema>;
export type NewOpendockArticleSetup = z.infer<
typeof newOpendockArticleSetupSchema
>;

View File

@@ -38,7 +38,7 @@ export const settings = pgTable(
},
(table) => [
// uniqueIndex('emailUniqueIndex').on(sql`lower(${table.email})`),
uniqueIndex("name").on(table.name),
uniqueIndex("settings_name_unique").on(table.name),
],
);

View File

@@ -62,7 +62,27 @@ router.get("/ehs/xml", (_, res) => {
return res.sendFile(xmlPath);
});
router.get("/upgrade/android/13", (_, res) => {
router.get("/android/upgrade/11", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_11-70-20.00-RG-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_11.zip"`,
);
return res.sendFile(apkPath);
});
router.get("/android/upgrade/13", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_13-51-16.00-TG-U00-STD-HEL-04.zip",
@@ -82,7 +102,7 @@ router.get("/upgrade/android/13", (_, res) => {
return res.sendFile(apkPath);
});
router.get("/upgrade/android/14", (_, res) => {
router.get("/android/upgrade/14", (_, res) => {
const apkPath = path.join(
downloadDir,
"HE_FULL_UPDATE_14-38-04.00-UG-U15-STD-HEL-04.zip",

View File

@@ -3,7 +3,7 @@ import { addHours } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
import { eq, sql } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { settings } from "../db/schema/settings.schema.js";
import { createLogger } from "../logger/logger.controller.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
@@ -27,6 +27,9 @@ type Releases = {
Quantity: number;
LineItemArticleWeight: number;
CustomerReleaseNumber: string;
DeliveryAddressDescription: string;
DeliveryAddressHumanReadableId: string;
AdditionalInformation1: string;
};
const timeZone = process.env.TIMEZONE as string;
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
@@ -73,6 +76,28 @@ const postRelease = async (release: Releases) => {
log.info({}, "Refreshing Auth Token");
await getToken();
}
// load validation checks
const defaultDock = await db.query.settings.findFirst({
where: (u, { eq }) => eq(u.name, "defaultLoadType"),
});
// check if the release has the data in it
const releaseLoadtypeCheck = (release.AdditionalInformation1 ?? "")
.toLowerCase()
.split(",")
.map((x) => x.trim())
.includes("drop");
const opendDockArticleCheck = await db.query.opendockArticleSetup.findFirst({
where: (table, { and, eq }) =>
and(
eq(table.av, release.LineItemArticleWeight),
eq(table.customer, release.DeliveryAddressHumanReadableId),
),
});
// TODO: add in docks from lst db here to make it more universal for the team
/**
* ReleaseState
* 0 = open
@@ -101,6 +126,7 @@ const postRelease = async (release: Releases) => {
: release.DeliveryState === 4 && "Completed",
userId: process.env.DEFAULT_CARRIER, // this should be the carrierid
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
// TODO: look in the remarks in the release and if its says
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber],
//refNumber: release.ReleaseNumber,
@@ -115,6 +141,19 @@ const postRelease = async (release: Releases) => {
},
units: null,
customFields: [
{
name: "strCustomer",
type: "str",
label: "Customer",
value: `${release.DeliveryAddressDescription}`,
description: "Who is the customer ",
placeholder: "",
dropDownValues: [],
minLengthOrValue: 1,
hiddenFromCarrier: false,
requiredForCarrier: false,
requiredForWarehouse: false,
},
{
name: "strArticle",
type: "str",
@@ -190,58 +229,179 @@ const postRelease = async (release: Releases) => {
if (existing) {
const id = existing.openDockAptId;
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
if (
(releaseLoadtypeCheck ||
opendDockArticleCheck?.loadType === "drop" ||
defaultDock?.value === "drop") &&
(release.DeliveryState === 0 || release.DeliveryState === 1)
) {
const setArrival = { ...newDockApt, status: "Arrived" };
// set to arrived
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
setArrival,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
// update the release in the db leaving as insert just incase something weird happened
// set to inprogress
await delay(1500);
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ error: e },
`Error updating the release: ${release.ReleaseNumber}`,
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ error: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
return;
}
} else {
try {
const response = await axios.patch(
`${process.env.OPENDOCK_URL}/appointment/${id}`,
newDockApt,
{
headers: {
"content-type": "application/json; charset=utf-8",
Authorization: `Bearer ${odToken.odToken}`,
},
},
);
if (response.status === 400) {
log.error({}, response.data.data.message);
return;
}
// update the release in the db leaving as insert just incase something weird happened
try {
await db
.insert(opendockApt)
.values({
release: release.ReleaseNumber,
openDockAptId: response.data.data.id,
appointment: response.data.data,
})
.onConflictDoUpdate({
target: opendockApt.release,
set: {
openDockAptId: response.data.data.id,
appointment: response.data.data,
upd_date: sql`NOW()`,
},
})
.returning();
log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) {
log.error(
{ stack: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
//console.info(newDockApt);
log.error(
{ stack: e.response.data },
`An error has occurred during patching of the release: ${release.ReleaseNumber}`,
);
return;
}
}
} else {
try {
@@ -287,13 +447,13 @@ const postRelease = async (release: Releases) => {
log.info({}, `${release.ReleaseNumber} was created`);
} catch (e) {
log.error({ error: e }, "Error creating new release");
log.error({ stack: e }, "Error creating new release");
}
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) {
log.error(
{ error: e?.response?.data },
"Error posting new release to opendock",
{ stack: e?.response?.data },
`Error posting new release to opendock, ${release.ReleaseNumber}`,
);
return;

View File

@@ -0,0 +1,198 @@
import { desc, eq, sql } from "drizzle-orm";
import { Router } from "express";
import z from "zod";
import { db } from "../db/db.controller.js";
import {
type NewOpendockArticleSetup,
opendockArticleSetup,
} from "../db/schema/opendock_articleSetup.js";
import { prodQuery } from "../prodSql/prodSqlQuery.controller.js";
import {
type SqlQuery,
sqlQuerySelector,
} from "../prodSql/prodSqlQuerySelector.utils.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";
const r = Router();
const newArticleLink = z.object({
av: z.number().int(),
description: z.string(),
customer: z.string().min(1).max(32),
customerDescription: z.string().min(2).max(100),
loadType: z
.enum(["drop", "live"])
.optional()
.describe("What roles are available to use."),
dock: z
//.record(z.string(), z.unknown())
.string()
.optional()
.describe(
"This allows us to add extra fields to the data to parse against",
),
});
r.post("/", async (req, res) => {
try {
const validated = newArticleLink.parse(req.body) as NewOpendockArticleSetup;
const newLink = await db
.insert(opendockArticleSetup)
.values({
av: validated.av,
description: validated.description,
customer: validated.customer,
customerDescription: validated.customerDescription,
loadType: validated.loadType,
dock: validated.dock,
add_user: req.user?.username ?? "lst_user",
})
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `${validated.av} was just added `,
data: newLink as any,
status: 200,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
// return res.status(400).json({
// error: "Validation failed",
// details: flattened,
// });
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Validation failed",
data: [flattened.fieldErrors],
status: 400, //connect.success ? 200 : 400,
});
}
return apiReturn(res, {
success: false,
level: "error", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Internal Server Error adding article link",
data: [err],
status: 400, //connect.success ? 200 : 400,
});
}
});
r.patch("/:id", async (req, res) => {
const { id } = req.params;
const updates: Record<string, unknown | null> = {};
if (req.body?.loadType !== undefined) {
updates.loadType = req.body.loadType;
}
if (req.body?.dock !== undefined) {
updates.dock = req.body.dock;
}
updates.upd_user = req.user?.username || "lst_user";
updates.upd_date = sql`NOW()`;
const updatedSetting = await db
.update(opendockArticleSetup)
.set(updates)
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `${updatedSetting[0]?.av} was just updated. `,
data: updatedSetting,
status: 200,
});
});
r.delete("/:id", async (req, res) => {
const { id } = req.params;
const removeLink = await db
.delete(opendockArticleSetup)
.where(eq(opendockArticleSetup.id, id))
.returning();
return apiReturn(res, {
success: false,
level: "info", //connect.success ? "info" : "error",
module: "opendock",
subModule: "articleCheck",
message: "Article link was deleted",
data: removeLink,
status: 200, //connect.success ? 200 : 400,
});
});
r.get("/", async (_, res) => {
const { data } = await tryCatch(
db
.select()
.from(opendockArticleSetup)
.orderBy(desc(opendockArticleSetup.customer))
.limit(1500),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `All links`,
data: data ?? [],
status: 200,
});
});
r.get("/customers/:av", async (req, res) => {
const { av } = req.params;
const avSQLQuery = sqlQuerySelector(`opendock.addressLink`) as SqlQuery;
if (!avSQLQuery.success) {
return apiReturn(res, {
success: true,
level: "error",
module: "opendock",
subModule: "articleCheck",
message: avSQLQuery.message,
data: [],
status: 200,
});
}
const { data } = await tryCatch(
prodQuery(
avSQLQuery.query.replace("[articleCheck]", av),
"openDock addressLink",
),
);
return apiReturn(res, {
success: true,
level: "info",
module: "opendock",
subModule: "articleCheck",
message: `All customers linked to av: ${av}`,
data: data as any,
status: 200,
});
});
export default r;

View File

@@ -1,6 +1,7 @@
import type { Express } from "express";
import { requireAuth } from "../middleware/auth.middleware.js";
import { featureCheck } from "../middleware/featureActive.middleware.js";
import articleCheck from "./opendock.articleCheck.route.js";
import getApt from "./opendockGetRelease.route.js";
@@ -13,4 +14,11 @@ export const setupOpendockRoutes = (baseUrl: string, app: Express) => {
requireAuth,
getApt,
);
app.use(
`${baseUrl}/api/opendock/articleCheck`,
featureCheck("opendock_sync"),
requireAuth,
articleCheck,
);
};

View File

@@ -1,7 +1,7 @@
import { desc, gte, sql } from "drizzle-orm";
import { Router } from "express";
import { db } from "../db/db.controller.js";
import { opendockApt } from "../db/schema/opendock.schema.js";
import { opendockApt } from "../db/schema/opendock_apt.schema.js";
import { apiReturn } from "../utils/returnHelper.utils.js";
import { tryCatch } from "../utils/trycatch.utils.js";

View File

@@ -1,8 +1,21 @@
/*
disables sql jobs.
*/
EXEC msdb.dbo.sp_update_job @job_name = N'[jobName]', @enabled = 0;
--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;
-- WHERE name = @JobName;
DECLARE @JobName NVARCHAR(128) = N'[jobName]';
IF EXISTS (
SELECT 1
FROM msdb.dbo.sysjobs
WHERE name = @JobName
)
BEGIN
EXEC msdb.dbo.sp_update_job
@job_name = @JobName,
@enabled = 0;
END

View File

@@ -0,0 +1,34 @@
/*
This will return all address with a sales price.
*/
WITH ranked AS (
SELECT
av.id,
av.humanReadableId as av,
av.Alias as description,
-- CONCAT(ad.HumanReadableId, ' - ',ad.Name) as customer ,
ad.HumanReadableId as customer,
ad.Name as customerDescription,
ROW_NUMBER() OVER (
PARTITION BY AddressId, sp.articleId
ORDER BY ValidAfter DESC
) AS rn
FROM [test1_AlplaPROD2.0_Read].[masterData].[SalesPrice] as sp (nolock)
/* av */
left join
[test1_AlplaPROD2.0_Read].[masterData].[Article] as av (nolock) on
av.id = sp.articleId
/* address */
left join
[test1_AlplaPROD2.0_Read].[masterData].[Address] as ad (nolock) on
ad.id = AddressId
)
SELECT *
FROM ranked
WHERE rn = 1
and ranked.av = '[articleCheck]'
order by customerDescription

View File

@@ -21,7 +21,7 @@ SELECT
,[MainMaterialId]
,[MainMaterialHumanReadableId]
,[MainMaterialDescription]
,[AdditionalInformation1]
,[AdditionalInformation1] -- we will use this to reference as the first check
,[AdditionalInformation2]
,[D365SupplierLot]
,[TradeUnits]
@@ -47,9 +47,9 @@ SELECT
,[PaymentTermsId]
,[PaymentTermsHumanReadableId]
,[PaymentTermsDescription]
,[Remark]
,[Remark]
,[DeliveryAddressId]
,[DeliveryAddressHumanReadableId]
,[DeliveryAddressHumanReadableId] --use this to validate with the new drop or live check
,[DeliveryAddressDescription]
,[DeliveryStreetName]
,[DeliveryAddressZip]

View File

@@ -1,16 +1,17 @@
use [test1_AlplaPROD2.0_Read]
SELECT
--JSON_VALUE(content, '$.EntityId') as labelId
JSON_VALUE(content, '$.EntityId') as labelId,
a.id
,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
,l.ArticleHumanReadableId as av
,l.ArticleDescription as alias
,PrintedCopies
,p.name as printerName
,RunningNumber
,COALESCE(l.ArticleHumanReadableId,e.ArticleHumanReadableId) as av
,COALESCE(l.ArticleDescription, av.Name) as alias
,COALESCE(l.PrintedCopies, 0) as PrintedCopies
,COALESCE(p.name,'External Label not tracked') as printerName
,COALESCE(l.RunningNumber, e.RunningNumber) as runningNumber
--,*
FROM [support].[AuditLog] (nolock) as a
@@ -18,10 +19,20 @@ left join
[labelling].[InternalLabel] (nolock) as l on
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
[masterData].[printer] (nolock) as p on
p.id = l.PrinterId
left join
[masterData].[article] (nolock) as av on
av.HumanReadableId = e.ArticleHumanReadableId
where message like '%reprint%'
and CreatedDateTime > DATEADD(minute, -[intervalCheck], SYSDATETIMEOFFSET())
and a.id > [ignoreList]

View File

@@ -357,6 +357,17 @@ const newSettings: NewSetting[] = [
roles: ["admin"],
seedVersion: 1,
},
{
name: "defaultLoadType",
value: "drop",
active: false,
description:
"What is the default load type we will use for creating new apt: drop or live are the current options.",
moduleName: "opendock",
settingType: "standard",
roles: ["admin"],
seedVersion: 1,
},
];
export const baseSettingValidationCheck = async () => {

View File

@@ -1,10 +1,12 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/access";
import { adminAc, defaultStatements } from "better-auth/plugins/admin/access";
export const statement = {
...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
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;
@@ -15,14 +17,22 @@ export const user = ac.newRole({
notifications: ["read", "create"],
});
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
});
export const admin = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
});
export const systemAdmin = ac.newRole({
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
notifications: ["read", "create", "share", "update", "delete", "readAll"],
...adminAc.statements,
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"],
});

View File

@@ -13,7 +13,7 @@ import {
//import { eq } from "drizzle-orm";
import { db } from "../db/db.controller.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 { sendEmail } from "./sendEmail.utils.js";
@@ -163,6 +163,7 @@ export const auth = betterAuth({
roles: {
admin,
user,
manager,
systemAdmin,
},
}),

View File

@@ -17,6 +17,7 @@ export const allowedOrigins = [
`http://${process.env.PROD_SERVER}:3100`, // temp
`http://usmcd1olp082:3000`,
`${process.env.EXTERNAL_URL}`, // internal docker
"chrome-extension://mddoackclclnbkmofficmmepfnadolfa",
];
export const lstCors = () => {
return cors({

View File

@@ -1,5 +1,4 @@
import { Tooltip as TooltipPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils";
@@ -40,7 +39,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
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,
)}
{...props}
@@ -52,4 +51,4 @@ function TooltipContent({
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

@@ -31,6 +31,13 @@ api.interceptors.response.use(
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 });
}

View File

@@ -1,5 +1,10 @@
import { redirect } from "@tanstack/react-router";
import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import {
adminClient,
genericOAuthClient,
usernameClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { ac, admin, manager, systemAdmin, user } from "./auth-permissions";
@@ -16,6 +21,7 @@ export const authClient = createAuthClient({
},
}),
genericOAuthClient(),
usernameClient(),
],
fetchOptions: {
onError() {

View File

@@ -1,9 +1,25 @@
import { createAccessControl } from "better-auth/plugins/access";
import { adminAc } from "better-auth/plugins/admin/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 = {
...defaultStatements,
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
quality: ["read", "create", "share", "update", "delete", "readAll"],
logistics: ["read", "create", "share", "update", "delete", "readAll"],
mobile: ["read", "create", "share", "update", "delete", "readAll"],
@@ -19,20 +35,22 @@ export const user = ac.newRole({
export const manager = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
});
export const admin = ac.newRole({
app: ["read", "create", "update"],
mobile: ["read", "create", "update"],
user: ["create", "update"],
});
export const systemAdmin = ac.newRole({
...adminAc.statements,
app: ["read", "create", "share", "update", "delete", "readAll"],
//user: ["ban"],
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"],
...adminAc.statements,
});
/* example usage

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

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

View File

@@ -9,6 +9,7 @@ import {
} from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff";
import { Separator } from "../../components/ui/separator";
export const Route = createFileRoute("/(auth)/user/signup")({
component: RouteComponent,
@@ -22,6 +23,7 @@ function RouteComponent() {
email: "",
password: "",
confirmPassword: "",
username: "",
},
onSubmit: async ({ value }) => {
if (value.password !== value.confirmPassword) {
@@ -33,6 +35,7 @@ function RouteComponent() {
name: value.name,
email: value.email,
password: value.password,
username: value.username ?? value.name,
callbackURL: `${window.location.origin}/lst/app`,
});
@@ -71,6 +74,15 @@ function RouteComponent() {
/>
)}
</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 */}
<form.AppField name="email">

View File

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

View File

@@ -10,6 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from "../../../components/ui/dialog";
import { api } from "../../../lib/apiHelper";
import { useAppForm } from "../../../lib/formSutff";
import { getScannerIds } from "../../../lib/queries/getScannerIds";
@@ -31,7 +32,7 @@ export default function NewScanUser({ refetch }: { refetch: any }) {
}
try {
const { data } = await axios.post(
const { data } = await api.post(
"/lst/api/mobile/auth/user",
{
name: value.name,

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

View File

@@ -21,6 +21,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client";
import { notificationSubs } from "../../lib/queries/notificationSubs";
import { notifications } from "../../lib/queries/notifications";
@@ -36,7 +37,7 @@ const updateNotifications = async (
//console.log(id, data);
try {
const res = await axios.patch(
`/lst/api/notification/${id}`,
`/notification/${id}`,
{ interval: data.interval },
{
withCredentials: true,
@@ -110,7 +111,7 @@ const NotificationTable = () => {
const removeNotification = async (ns: any) => {
try {
const res = await axios.delete(`/lst/api/notification/sub`, {
const res = await api.delete(`/notification/sub`, {
withCredentials: true,
data: {
userId: ns.userId,
@@ -168,7 +169,7 @@ const NotificationTable = () => {
setActiveToggle(e);
try {
const res = await axios.patch(
const res = await api.patch(
`/lst/api/notification/${i.row.original.id}`,
{
active: !activeToggle,

View File

@@ -8,6 +8,7 @@ import { Suspense, useState } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
import { Spinner } from "../../components/ui/spinner";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client";
import { getScanUsers } from "../../lib/queries/getScanUsers";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
@@ -19,7 +20,13 @@ import NewScanUser from "./-components/NewScanUser";
export const Route = createFileRoute("/admin/scanUsers")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin", "manager"];
//const allowedRole = ["systemAdmin", "admin", "manager"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
mobile: ["create"],
},
});
if (!session?.user) {
throw redirect({
@@ -30,7 +37,9 @@ export const Route = createFileRoute("/admin/scanUsers")({
});
}
if (!allowedRole.includes(session.user.role as string)) {
//if (!allowedRole.includes(session.user.role as string)) {
if (!canAccess) {
throw redirect({
to: "/",
});
@@ -47,7 +56,7 @@ const updateSettings = async (
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/mobile/auth/user/${id}`, data, {
const res = await axios.patch(`/mobile/auth/user/${id}`, data, {
withCredentials: true,
timeout: 15000,
validateStatus: () => true,
@@ -123,7 +132,7 @@ const ScanUserTable = () => {
<Button
type="button"
onClick={async () => {
const { data } = await axios.get("/lst/api/mobile/pin/new");
const { data } = await api.get("/mobile/pin/new");
updateSetting.mutate({
id: row.original.id,
field: "pinNumber",
@@ -171,7 +180,7 @@ const ScanUserTable = () => {
setActiveToggle(true);
try {
const res = await axios.delete(
const res = await api.delete(
`/lst/api/mobile/auth/user/${i.row.original.id}`,
{

View File

@@ -1,7 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { format } from "date-fns-tz";
import { CircleFadingArrowUp, Trash } from "lucide-react";
import { Suspense, useState } from "react";
@@ -14,6 +14,7 @@ import {
TooltipTrigger,
} from "../../components/ui/tooltip";
import { useSocketRoom } from "../../hooks/socket.io.hook";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client";
import { servers } from "../../lib/queries/servers";
import LstTable from "../../lib/tableStuff/LstTable";
@@ -111,19 +112,20 @@ const ServerTable = () => {
const [activeToggle, setActiveToggle] = useState(false);
const onToggle = async () => {
setActiveToggle(true);
toast.success(
`${i.row.original.name} just started the upgrade monitor logs for errors.`,
);
setActiveToggle(activeToggle);
try {
const res = await axios.post(
`/lst/api/admin/build/updateServer`,
const res = await api.post(
`/admin/build/updateServer`,
{
server: i.row.original.server,
destination: i.row.original.serverLoc,
token: i.row.original.plantToken,
},
{ withCredentials: true },
{ withCredentials: true, timeout: 5 * 60 * 1000 },
);
if (res.data.success) {
@@ -218,8 +220,8 @@ function RouteComponent() {
];
const triggerBuild = async () => {
try {
const res = await axios.post(
`/lst/api/admin/build/release`,
const res = await api.post(
`/admin/build/release`,
{
withCredentials: true,

View File

@@ -1,8 +1,6 @@
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import axios from "axios";
import { Suspense, useMemo } from "react";
import { toast } from "sonner";
@@ -24,6 +22,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { api } from "../../lib/apiHelper";
import { authClient } from "../../lib/auth-client";
import { getSettings } from "../../lib/queries/getSettings";
import EditableCellInput from "../../lib/tableStuff/EditableCellInput";
@@ -48,7 +47,7 @@ const updateSettings = async (
) => {
//console.log(id, data);
try {
const res = await axios.patch(`/lst/api/settings/${id}`, data, {
const res = await api.patch(`/settings/${id}`, data, {
withCredentials: true,
});
toast.success(`Setting just updated`);

View File

@@ -1,20 +1,43 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createColumnHelper } from "@tanstack/react-table";
import { format } from "date-fns-tz";
import { KeyRound } from "lucide-react";
import { Suspense } from "react";
import { toast } from "sonner";
import { Button } from "../../components/ui/button";
import { authClient, useSession } from "../../lib/auth-client";
import { Input } from "../../components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip";
import { authClient } from "../../lib/auth-client";
import { selectableRoles } from "../../lib/auth-permissions";
import { getUsers } from "../../lib/queries/getUsers";
import { permissionQuery } from "../../lib/queries/permsCheck";
import LstTable from "../../lib/tableStuff/LstTable";
import SearchableHeader from "../../lib/tableStuff/SearchableHeader";
import SkellyTable from "../../lib/tableStuff/SkellyTable";
import { trackLstEvent } from "../../lib/umami.utils";
import NewUser from "./-components/Newuser";
export const Route = createFileRoute("/admin/users")({
beforeLoad: async ({ location }) => {
const { data: session } = await authClient.getSession();
const allowedRole = ["systemAdmin", "admin"];
// const allowedRole = ["systemAdmin", "admin"];
const canAccess = await authClient.admin.hasPermission({
permissions: {
user: ["create"],
},
});
if (!session?.user) {
throw redirect({
@@ -25,7 +48,8 @@ export const Route = createFileRoute("/admin/users")({
});
}
if (!allowedRole.includes(session.user.role as string)) {
//if (!allowedRole.includes(session.user.role as string)) {
if (!canAccess) {
throw redirect({
to: "/",
});
@@ -37,8 +61,51 @@ export const Route = createFileRoute("/admin/users")({
});
const UserTable = () => {
const { data } = useSuspenseQuery(getUsers());
const { data: session } = useSession();
const { data, refetch } = useSuspenseQuery(getUsers());
//const { data: session } = useSession();
const { data: canImpersonate = false } = useQuery(
permissionQuery({
user: ["impersonate"],
}),
);
const { data: canUpdate = false } = useQuery(
permissionQuery({
user: ["update"],
}),
);
const updatePassword = useMutation({
mutationFn: async ({ user, password }: { user: any; password: string }) => {
return authClient.admin.setUserPassword({
userId: user.id,
newPassword: password,
});
},
onSuccess: () => {
toast.success("Password updated");
},
onError: (error) => {
toast.error(error.message);
},
});
const handleRoleChange = async (row: any, newRole: string) => {
//console.log("update this user", row, newRole);
const { data, error } = await authClient.admin.updateUser({
userId: row.id,
data: { role: newRole },
});
if (error) {
console.error(error);
toast.error(error.message);
return;
}
toast.success(`${data.name}, role was just changed to: ${newRole}`);
refetch();
};
const columnHelper = createColumnHelper<any>();
@@ -50,6 +117,13 @@ const UserTable = () => {
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("username", {
header: ({ column }) => (
<SearchableHeader column={column} title="Username" searchable={true} />
),
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("email", {
header: ({ column }) => (
<SearchableHeader column={column} title="Email" searchable={true} />
@@ -57,27 +131,113 @@ const UserTable = () => {
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
// columnHelper.accessor("role", {
// header: ({ column }) => (
// <SearchableHeader column={column} title="Role" searchable={false} />
// ),
// filterFn: "includesString",
// cell: (i) => i.getValue(),
// }),
columnHelper.accessor("role", {
header: ({ column }) => (
<SearchableHeader column={column} title="Role" searchable={false} />
),
header: ({ column }) => <SearchableHeader column={column} title="Role" />,
filterFn: "includesString",
cell: (i) => i.getValue(),
}),
columnHelper.accessor("updatedAt", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Updated at"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
cell: ({ row, getValue }) => {
const currentRole = getValue();
return (
<Select
value={currentRole}
onValueChange={(newRole) => {
handleRoleChange(row.original, newRole);
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{selectableRoles.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
},
}),
];
if (session && session.user.role === "systemAdmin") {
if (canUpdate) {
columns.push(
columnHelper.accessor("changePassword", {
header: ({ column }) => (
<SearchableHeader column={column} title="Change Password" />
),
filterFn: "includesString",
cell: ({ row }) => {
return (
<div className="flex flex-row items-center gap-2">
<Input
type="password"
placeholder="New password"
className="w-[200px]"
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const password = e.currentTarget.value.trim();
if (!password) return;
updatePassword.mutate({
user: row.original,
password,
});
e.currentTarget.value = "";
}}
/>
<Tooltip>
<TooltipTrigger>
<Button
size="icon"
variant="outline"
onClick={(e) => {
const input =
e.currentTarget.parentElement?.querySelector("input");
const password = input?.value.trim();
if (!password) return;
updatePassword.mutate({
user: row.original,
password,
});
if (input) {
input.value = "";
}
}}
>
<KeyRound className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
Update Password, fill out and press enter or update here
</p>
</TooltipContent>
</Tooltip>
</div>
);
},
}),
);
}
if (canImpersonate) {
columns.push(
columnHelper.accessor("banned", {
header: ({ column }) => (
@@ -126,7 +286,28 @@ const UserTable = () => {
);
}
return <LstTable data={data} columns={columns} pageSize={50} />;
columns.push(
columnHelper.accessor("updatedAt", {
header: ({ column }) => (
<SearchableHeader
column={column}
title="Updated at"
searchable={false}
/>
),
filterFn: "includesString",
cell: (i) => format(i.getValue(), "M/d/yyyy HH:mm"),
}),
);
return (
<div>
<div className="flex justify-end m-2">
<NewUser refetch={refetch} />
</div>
<LstTable data={data} columns={columns} pageSize={50} />
</div>
);
};
function RouteComponent() {

View File

@@ -0,0 +1,3 @@
DROP INDEX "opendock_apt_release_idx";--> statement-breakpoint
DROP INDEX "name";--> statement-breakpoint
CREATE UNIQUE INDEX "settings_name_unique" ON "settings" USING btree ("name");

View File

@@ -0,0 +1,14 @@
CREATE TYPE "public"."load_type" AS ENUM('drop', 'live');--> statement-breakpoint
CREATE TABLE "opendock_article_setup" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"av" integer NOT NULL,
"description" text NOT NULL,
"customer" text NOT NULL,
"load_type" "load_type" DEFAULT 'drop' NOT NULL,
"dock" text NOT NULL,
"upd_date" timestamp DEFAULT now() NOT NULL,
"upd_user" text DEFAULT 'lst-system' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"add_user" text DEFAULT 'lst-system' NOT NULL,
CONSTRAINT "opendock_article_setup_av_unique" UNIQUE("av")
);

View File

@@ -0,0 +1 @@
ALTER TABLE "opendock_article_setup" ADD COLUMN "customer_description" text NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -372,6 +372,27 @@
"when": 1778533475205,
"tag": "0052_numerous_wasp",
"breakpoints": true
},
{
"idx": 53,
"version": "7",
"when": 1779381841381,
"tag": "0053_petite_thunderbird",
"breakpoints": true
},
{
"idx": 54,
"version": "7",
"when": 1779381875298,
"tag": "0054_talented_nocturne",
"breakpoints": true
},
{
"idx": 55,
"version": "7",
"when": 1779399354404,
"tag": "0055_nosy_amphibian",
"breakpoints": true
}
]
}

79
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "lst_v3",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lst_v3",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"license": "ISC",
"dependencies": {
"@dotenvx/dotenvx": "^1.57.0",
@@ -16,6 +16,7 @@
"axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"chokidar": "^5.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
"croner": "^10.0.1",
@@ -3801,28 +3802,18 @@
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/cli-cursor": {
@@ -10734,16 +10725,16 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/real-require": {
@@ -11978,6 +11969,44 @@
}
}
},
"node_modules/ts-node-dev/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/ts-node-dev/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/tsconfig": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "lst_v3",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"description": "The tool that supports us in our everyday alplaprod",
"main": "index.js",
"scripts": {
@@ -71,6 +71,7 @@
"axios": "^1.13.6",
"bcryptjs": "^3.0.3",
"better-auth": "^1.5.5",
"chokidar": "^5.0.0",
"concurrently": "^9.2.1",
"cors": "^2.8.6",
"croner": "^10.0.1",

View File

@@ -180,6 +180,23 @@ function Update-Server {
Stop-Service -DisplayName $app_name -Force
Start-Sleep -Seconds 1
Write-Host "Removing old server"
# Remove old dist folder before extracting
$DistPath = Join-Path $LocalPath "dist"
if (Test-Path $DistPath) {
Write-Host "Removing old dist folder..."
try {
Remove-Item $DistPath -Recurse -Force
}
catch {
Write-Host "Failed to remove dist folder: $_"
exit 1
}
}
Write-Host "Unzipping the folder..."
try {