Compare commits

..

2 Commits

15 changed files with 1004 additions and 362 deletions

View File

@@ -4,23 +4,24 @@ meta {
seq: 7 seq: 7
} }
post { patch {
url: {{url}}/lst/api/admin/:userID/grant url: {{url}}/lst/api/admin/:userID/grant
body: json body: json
auth: inherit auth: inherit
} }
params:path { params:path {
userID: 0hlO48C7Jw1J804FxrCnonK userID: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6
} }
body:json { body:json {
{ {
"module":"users", "module":"siloAdjustments",
"role":"admin" "role":"viewer"
} }
} }
settings { settings {
encodeUrl: true encodeUrl: true
timeout: 0
} }

View File

@@ -0,0 +1,27 @@
meta {
name: RevokeRole by ID
type: http
seq: 3
}
post {
url: {{url}}/lst/api/admin/:userID/grant
body: json
auth: inherit
}
params:path {
userID: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6
}
body:json {
{
"module":"siloAdjustments",
"role":"viewer"
}
}
settings {
encodeUrl: true
timeout: 0
}

View File

@@ -4,22 +4,30 @@ import { requireAuth } from "../../pkg/middleware/authMiddleware.js";
//admin routes //admin routes
import users from "./routes/getUserRoles.js"; import users from "./routes/getUserRoles.js";
import grantRoles from "./routes/grantRole.js"; import grantRoles from "./routes/grantRole.js";
import revokeRoles from "./routes/revokeRole.js";
import servers from "./routes/servers/serverRoutes.js"; import servers from "./routes/servers/serverRoutes.js";
export const setupAdminRoutes = (app: Express, basePath: string) => { export const setupAdminRoutes = (app: Express, basePath: string) => {
app.use( app.use(
basePath + "/api/admin/server", // will pass bc system admin but this is just telling us we need this basePath + "/api/admin/server", // will pass bc system admin but this is just telling us we need this
servers servers,
); );
app.use( app.use(
basePath + "/api/admin/users", basePath + "/api/admin/users",
requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this
users users,
); );
app.use(
basePath + "/api/admin", app.use(
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this basePath + "/api/admin",
grantRoles requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
); grantRoles,
);
app.use(
basePath + "/api/admin",
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
revokeRoles,
);
}; };

View File

@@ -1,74 +1,82 @@
import { Router } from "express";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { tryCatch } from "../../../pkg/utils/tryCatch.js"; import { Router } from "express";
import { db } from "../../../pkg/db/db.js";
import z from "zod"; import z from "zod";
import { db } from "../../../pkg/db/db.js";
import { userRoles } from "../../../pkg/db/schema/user_roles.js"; import { userRoles } from "../../../pkg/db/schema/user_roles.js";
import { createLogger } from "../../../pkg/logger/logger.js"; import { createLogger } from "../../../pkg/logger/logger.js";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
const roleSchema = z.object({ const roleSchema = z.object({
module: z.enum([ module: z.enum([
"users", "users",
"system", "system",
"ocp", "ocp",
"siloAdjustments", "siloAdjustments",
"demandManagement", "demandManagement",
"logistics", "logistics",
"production", "production",
"quality", "quality",
"eom", "eom",
"forklifts", "forklifts",
]), ]),
role: z.enum(["admin", "manager", "supervisor", "test,", "viewer"]), role: z.enum([
"systemAdmin",
"admin",
"manager",
"supervisor",
"tester",
"user",
"viewer",
]),
}); });
const router = Router(); const router = Router();
router.post("/:userId/grant", async (req: Request, res: Response) => { router.patch("/:userId/grant", async (req: Request, res: Response) => {
const log = createLogger({ const log = createLogger({
module: "admin", module: "admin",
subModule: "grantRoles", subModule: "grantRoles",
}); });
const userId = req.params.userId; const userId = req.params.userId;
console.log(userId);
try { try {
const validated = roleSchema.parse(req.body); const validated = roleSchema.parse(req.body);
const data = await db const data = await db
.insert(userRoles) .insert(userRoles)
.values({ .values({
userId, userId: userId,
module: validated.module, module: validated.module,
role: validated.role, role: validated.role,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: [userRoles.userId, userRoles.module], target: [userRoles.userId, userRoles.module],
set: { module: validated.module, role: validated.role }, set: { module: validated.module, role: validated.role },
}); });
log.info( log.info(
{}, {},
`Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}` `Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}`,
); );
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`, message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`,
data, data,
}); });
} catch (err) { } catch (err) {
if (err instanceof z.ZodError) { if (err instanceof z.ZodError) {
const flattened = z.flattenError(err); const flattened = z.flattenError(err);
return res.status(400).json({ return res.status(400).json({
error: "Validation failed", error: "Validation failed",
details: flattened, details: flattened,
}); });
} }
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: "Invalid input please try again.", message: "Invalid input please try again.",
}); error: err,
} });
}
}); });
export default router; export default router;

View File

@@ -0,0 +1,71 @@
import { and, eq } from "drizzle-orm";
import type { Request, Response } from "express";
import { Router } from "express";
import z from "zod";
import { db } from "../../../pkg/db/db.js";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
import { createLogger } from "../../../pkg/logger/logger.js";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
const roleSchema = z.object({
module: z.enum([
"users",
"system",
"ocp",
"siloAdjustments",
"demandManagement",
"logistics",
"production",
"quality",
"eom",
"forklifts",
]),
});
const router = Router();
router.patch("/:userId/revoke", async (req: Request, res: Response) => {
const log = createLogger({
module: "admin",
subModule: "grantRoles",
});
const userId = req.params.userId;
try {
const validated = roleSchema.parse(req.body);
const data = await db
.delete(userRoles)
.where(
and(
eq(userRoles.userId, userId),
eq(userRoles.module, validated.module),
),
);
log.info(
{},
`Module: ${validated.module}, was just revoked fron userID: ${userId}`,
);
return res.status(200).json({
success: true,
message: `Module: ${validated.module}, was just revoked fron userID: ${userId}`,
data,
});
} catch (err) {
if (err instanceof z.ZodError) {
const flattened = z.flattenError(err);
return res.status(400).json({
error: "Validation failed",
details: flattened,
});
}
return res.status(400).json({
success: false,
message: "Invalid input please try again.",
error: err,
});
}
});
export default router;

View File

@@ -25,6 +25,7 @@
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.89.0",
"@tanstack/react-router": "^1.131.36", "@tanstack/react-router": "^1.131.36",
"@tanstack/react-router-devtools": "^1.131.36", "@tanstack/react-router-devtools": "^1.131.36",
"@tanstack/react-table": "^8.21.3",
"@types/react-calendar-timeline": "^0.28.6", "@types/react-calendar-timeline": "^0.28.6",
"axios": "^1.12.2", "axios": "^1.12.2",
"better-auth": "^1.3.11", "better-auth": "^1.3.11",
@@ -3372,6 +3373,26 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.3"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/router-core": { "node_modules/@tanstack/router-core": {
"version": "1.131.36", "version": "1.131.36",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.36.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.36.tgz",
@@ -3533,6 +3554,19 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-file-routes": { "node_modules/@tanstack/virtual-file-routes": {
"version": "1.131.2", "version": "1.131.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.131.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.131.2.tgz",

View File

@@ -27,6 +27,7 @@
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.89.0",
"@tanstack/react-router": "^1.131.36", "@tanstack/react-router": "^1.131.36",
"@tanstack/react-router-devtools": "^1.131.36", "@tanstack/react-router-devtools": "^1.131.36",
"@tanstack/react-table": "^8.21.3",
"@types/react-calendar-timeline": "^0.28.6", "@types/react-calendar-timeline": "^0.28.6",
"axios": "^1.12.2", "axios": "^1.12.2",
"better-auth": "^1.3.11", "better-auth": "^1.3.11",

View File

@@ -1,81 +1,76 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { useAuth, useLogout } from "../../lib/authClient"; import { useAuth, useLogout } from "../../lib/authClient";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { ModeToggle } from "../mode-toggle"; import { ModeToggle } from "../mode-toggle";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
export default function Nav() { export default function Nav() {
const { session } = useAuth(); const { session } = useAuth();
const logout = useLogout(); const logout = useLogout();
return ( return (
<nav className="flex justify-end w-full shadow "> <nav className="flex justify-end w-full shadow ">
<div className="m-2 flex flex-row gap-1"> <div className="m-2 flex flex-row gap-1">
<div className="m-1"> <div className="m-1">
<ModeToggle /> <ModeToggle />
</div> </div>
<div className="m-1"> <div className="m-1">
<Button> <Button>
<a <a href={`${window.location.origin}/lst/d`} target="_blank">
href={`${window.location.origin}/lst/d`} LST - Docs
target="_blank" </a>
> </Button>
LST - Docs </div>
</a>
</Button>
</div>
{session ? ( {session ? (
<div className="m-1"> <div className="m-1">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Avatar> <Avatar>
<AvatarImage <AvatarImage
src="https://github.com/evilrabbit.png" src="https://github.com/evilrabbit.png"
alt="@evilrabbit" alt="@evilrabbit"
/> />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
</Avatar> </Avatar>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel> <DropdownMenuLabel>
Hello {session.user?.username} Hello {session.user?.username}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem>
{/* <Link to="/passwordChange"> {/* <Link to="/passwordChange">
Password Change Password Change
</Link> */} </Link> */}
</DropdownMenuItem> </DropdownMenuItem>
{/* <DropdownMenuItem>Billing</DropdownMenuItem> {/* <DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem> <DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem> */} <DropdownMenuItem>Subscription</DropdownMenuItem> */}
<hr className="solid"></hr> <hr className="solid"></hr>
<DropdownMenuItem> <DropdownMenuItem>
<div className="m-auto"> <div className="m-auto">
<button onClick={() => logout()}> <button onClick={() => logout()}>Logout</button>
Logout </div>
</button> </DropdownMenuItem>
</div> </DropdownMenuContent>
</DropdownMenuItem> </DropdownMenu>
</DropdownMenuContent> </div>
</DropdownMenu> ) : (
</div> <div className="m-1">
) : ( <Button>
<div className="m-1"> <Link to="/login">Login</Link>
<Button> </Button>
<Link to="/login">Login</Link> </div>
</Button> )}
</div> </div>
)} </nav>
</div> );
</nav>
);
} }

View File

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

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,17 @@
import { queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getUsers() {
return queryOptions({
queryKey: ["getUsers"],
queryFn: () => fetchSession(),
staleTime: 5000,
refetchOnWindowFocus: true,
});
}
const fetchSession = async () => {
const { data } = await axios.post("/lst/api/admin/users");
return data.data;
};

View File

@@ -1,81 +1,81 @@
import {
createRootRouteWithContext,
Outlet,
useRouter,
} from "@tanstack/react-router";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { Toaster } from "sonner"; import {
import Cookies from "js-cookie"; createRootRouteWithContext,
import { SessionGuard } from "../lib/providers/SessionProvider"; Outlet,
import Nav from "../components/navBar/Nav"; useRouter,
import { ThemeProvider } from "../lib/providers/theme-provider"; } from "@tanstack/react-router";
import { SidebarProvider } from "../components/ui/sidebar";
import SideBarNav from "../components/navBar/SideBarNav";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { userAccess } from "../lib/authClient";
import mobile from "is-mobile"; import mobile from "is-mobile";
import Cookies from "js-cookie";
import { useEffect } from "react"; import { useEffect } from "react";
import { Toaster } from "sonner";
import Nav from "../components/navBar/Nav";
import SideBarNav from "../components/navBar/SideBarNav";
import { SidebarProvider, SidebarTrigger } from "../components/ui/sidebar";
import { userAccess } from "../lib/authClient";
import { SessionGuard } from "../lib/providers/SessionProvider";
import { ThemeProvider } from "../lib/providers/theme-provider";
import { coreSocket } from "../lib/socket.io/socket"; import { coreSocket } from "../lib/socket.io/socket";
interface RootRouteContext { interface RootRouteContext {
queryClient: QueryClient; queryClient: QueryClient;
//user: User | null; //user: User | null;
//login: (user: User) => void; //login: (user: User) => void;
//logout: () => void; //logout: () => void;
} }
const RootLayout = () => { const RootLayout = () => {
//const { logout, login } = Route.useRouteContext(); //const { logout, login } = Route.useRouteContext();
const defaultOpen = Cookies.get("sidebar_state") === "true"; const defaultOpen = Cookies.get("sidebar_state") === "true";
const router = useRouter(); const router = useRouter();
// console.log(mobile({ featureDetect: true, tablet: true })); // console.log(mobile({ featureDetect: true, tablet: true }));
// if mobile lets move to the mobile section. // if mobile lets move to the mobile section.
useEffect(() => { useEffect(() => {
if (mobile({ featureDetect: true, tablet: true })) { if (mobile({ featureDetect: true, tablet: true })) {
router.navigate({ to: "/m" }); router.navigate({ to: "/m" });
} }
coreSocket.on("connect", () => { coreSocket.on("connect", () => {
console.log("✅ Connected:", coreSocket.id); console.log("✅ Connected:", coreSocket.id);
}); });
coreSocket.on("disconnect", () => { coreSocket.on("disconnect", () => {
console.log("🔴 Disconnected"); console.log("🔴 Disconnected");
}); });
return () => { return () => {
coreSocket.off("connect"); coreSocket.off("connect");
coreSocket.off("disconnect"); coreSocket.off("disconnect");
}; };
}, []); }, []);
return ( return (
<div> <div>
<SessionGuard> <SessionGuard>
<ThemeProvider> <ThemeProvider>
<div className="flex flex-col h-screen overflow-hidden"> <div className="flex flex-col h-screen overflow-hidden">
<Nav /> <Nav />
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<SidebarProvider defaultOpen={defaultOpen}> <SidebarProvider defaultOpen={defaultOpen}>
<SideBarNav /> <SideBarNav />
<div className="flex-2 overflow-y-auto">
<Outlet /> <div className="flex-2 overflow-y-auto">
</div> <Outlet />
</SidebarProvider> </div>
</div> </SidebarProvider>
<Toaster expand richColors closeButton /> </div>
{userAccess(null, ["systemAdmin"]) && ( <Toaster expand richColors closeButton />
<TanStackRouterDevtools position="bottom-right" /> {userAccess(null, ["systemAdmin"]) && (
)} <TanStackRouterDevtools position="bottom-right" />
</div> )}
</ThemeProvider> </div>
</SessionGuard> </ThemeProvider>
</div> </SessionGuard>
); </div>
);
}; };
export const Route = createRootRouteWithContext<RootRouteContext>()({ export const Route = createRootRouteWithContext<RootRouteContext>()({
component: RootLayout, component: RootLayout,
}); });

View File

@@ -0,0 +1,144 @@
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import { api } from "../../../lib/axiosAPI";
const modules: string[] = [
"users",
"system",
"ocp",
"siloAdjustments",
"demandManagement",
"logistics",
"production",
"quality",
"eom",
"forklifts",
];
const roles: string[] = [
"systemAdmin",
"admin",
"manager",
"supervisor",
"tester",
"user",
"viewer",
];
export default function ExpandedRow({ row }: { row: any }) {
const user = row.original;
const existingRolesMap = Object.fromEntries(
(user.roles || []).map((r: { module: string; role: string }) => [
r.module,
r.role,
]),
);
// local state for selections
const [selectedRoles, setSelectedRoles] = useState<Record<string, string>>(
() =>
Object.fromEntries(
modules.map((m) => [m, existingRolesMap[m] || "viewer"]),
),
);
const onSubmitRole = async (module: string) => {
const role = selectedRoles[module];
// console.log("Saving module role:", {
// module,
// role,
// user,
// });
try {
const result = await api.patch(`/api/admin/${user.id}/grant`, {
module: module,
role: role,
});
if (result.status === 200) {
toast.success(
`${user.username} was just granted ${role} on module ${module}`,
);
}
} catch (error) {
console.log(error);
toast.error(
"There was an error granting the user a role if this continues please contact your admin.",
);
}
};
const handleSelectChange = (module: string, value: string) => {
setSelectedRoles((prev) => ({ ...prev, [module]: value }));
};
const onDeleteRole = async (module: string) => {
try {
const result = await api.patch(`/api/admin/${user.id}/revoke`, {
module: module,
});
if (result.status === 200) {
toast.success(`${user.username} no longer has access to ${module}`);
setSelectedRoles((prev) => ({ ...prev, [module]: "viewer" }));
}
} catch (error) {
console.log(error);
toast.error(
"There was an error granting the user a role if this continues please contact your admin.",
);
}
};
return (
<div className="p-4 bg-muted w-128">
<div className="">
{modules.map((i) => {
return (
<div
key={i}
className="flex flex-row items-center justify-between gap-4 border-b py-2"
>
<span className="text-sm font-medium capitalize w-40">{i}</span>
<Select
value={selectedRoles[i]}
onValueChange={(v) => handleSelectChange(i, v)}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
{roles.map((r) => (
<SelectItem key={r} value={r}>
{r}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => onSubmitRole(i)} size="sm">
Grant
</Button>
<Button
onClick={() => onDeleteRole(i)}
size="sm"
variant="destructive"
>
Revoke
</Button>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -1,9 +1,215 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import {
ArrowDown,
ArrowUp,
ChevronDown,
ChevronRight,
Mail,
User,
} from "lucide-react";
import React, { useState } from "react";
import { Button } from "../../../../components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../../../components/ui/table";
import { getUsers } from "../../../../lib/querys/admin/getUsers";
import ExpandedRow from "../../-components/ExpandedRow";
type User = {
username: string;
email: string;
roles: string | null;
};
export const Route = createFileRoute("/_adminLayout/admin/_users/users")({ export const Route = createFileRoute("/_adminLayout/admin/_users/users")({
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
return <div className="">Hello "/_admin/admin/users "!</div>; const { data, isLoading } = useQuery(getUsers());
const [sorting, setSorting] = useState<SortingState>([]);
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor("username", {
cell: (i) => i.getValue(),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">
<User />
Username
</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
}),
columnHelper.accessor("email", {
cell: (i) => i.getValue(),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row gap-2">
<Mail />
Email
</span>
{column.getIsSorted() === "asc" ? (
<ArrowUp className="ml-2 h-4 w-4" />
) : (
<ArrowDown className="ml-2 h-4 w-4" />
)}
</Button>
);
},
}),
columnHelper.accessor("roles", {
header: () => <span>Roles</span>,
cell: ({ row }) => {
return row.getCanExpand() ? (
<button
{...{
onClick: row.getToggleExpandedHandler(),
style: { cursor: "pointer" },
}}
>
{row.getIsExpanded() ? (
<span className="flex flex-row gap-2">
Roles <ChevronDown />
</span>
) : (
<span className="flex flex-row gap-2">
Roles <ChevronRight />
</span>
)}
</button>
) : (
"No expanding"
);
},
}),
];
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
getRowCanExpand: () => true,
state: {
sorting,
},
});
if (isLoading) {
return <div className="m-auto">Loading user data</div>;
}
// render the roles card and make ts happy by not including it in the useReactTable hook
const renderSubComponent = ({ row }: { row: any }) => (
<ExpandedRow row={row} />
);
return (
<div className="p-4">
<div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{row.getIsExpanded() && (
<TableRow>
<TableCell colSpan={row.getVisibleCells().length}>
{renderSubComponent({ row })}
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
} }

View File

@@ -1,155 +1,171 @@
import { ControllerManager } from "st-ethernet-ip"; import { ControllerManager } from "st-ethernet-ip";
import { createLog } from "../../../../logger/logger.js";
import { getMac } from "../../../utils/getMachineId.js"; import { getMac } from "../../../utils/getMachineId.js";
import { labelingProcess } from "../../labeling/labelProcess.js"; import { labelingProcess } from "../../labeling/labelProcess.js";
export const createPlcMonitor = (config: any) => { export const createPlcMonitor = (config: any) => {
let cm: any; let cm: any;
let controllers: any = {}; let controllers: any = {};
let stats: any = {}; let stats: any = {};
let isRunning = false; let isRunning = false;
const nowISO = () => { const nowISO = () => {
return new Date().toISOString(); return new Date().toISOString();
}; };
const start = () => { const start = () => {
if (isRunning) return; if (isRunning) return;
cm = new ControllerManager(); cm = new ControllerManager();
config.controllers.forEach((cfg: any) => { config.controllers.forEach((cfg: any) => {
const plc: any = cm.addController( const plc: any = cm.addController(
cfg.ip, cfg.ip,
cfg.slot, cfg.slot,
cfg.rpi, cfg.rpi,
true, true,
cfg.retrySP || 3000 cfg.retrySP || 3000,
); );
plc.connect(); plc.connect();
controllers[cfg.id] = plc; controllers[cfg.id] = plc;
// initialize stats // initialize stats
stats[cfg.id] = { stats[cfg.id] = {
id: cfg.id, id: cfg.id,
ip: cfg.ip, ip: cfg.ip,
slot: cfg.slot, slot: cfg.slot,
scanRate: cfg.rpi, scanRate: cfg.rpi,
connected: false, connected: false,
lastConnectedAt: null, lastConnectedAt: null,
lastDisconnectedAt: null, lastDisconnectedAt: null,
reconnectCount: 0, reconnectCount: 0,
}; };
// Add tags // Add tags
cfg.tags.forEach((tag: any) => plc.addTag(tag)); cfg.tags.forEach((tag: any) => plc.addTag(tag));
// Events // Events
plc.on("Connected", () => { plc.on("Connected", () => {
const s = stats[cfg.id]; const s = stats[cfg.id];
s.connected = true; s.connected = true;
s.lastConnectedAt = nowISO(); s.lastConnectedAt = nowISO();
if (s.lastDisconnectedAt) { if (s.lastDisconnectedAt) {
s.reconnectCount++; s.reconnectCount++;
} }
console.log(`[${cfg.id}] Connected @ ${cfg.ip}:${cfg.slot}`); createLog(
}); "info",
"zechette",
"ocp",
`[${cfg.id}] Connected @ ${cfg.ip}:${cfg.slot}`,
);
});
plc.on("Disconnected", () => { plc.on("Disconnected", () => {
const s = stats[cfg.id]; const s = stats[cfg.id];
s.connected = false; s.connected = false;
s.lastDisconnectedAt = nowISO(); s.lastDisconnectedAt = nowISO();
console.log(`[${cfg.id}] Disconnected`); createLog("info", "zechette", "ocp", `[${cfg.id}] Disconnected`);
}); });
plc.on("error", (err: any) => { plc.on("error", (err: any) => {
console.error(`[${cfg.id}] Error:`, err.message); createLog(
}); "error",
"zechette",
"ocp",
`[${cfg.id}] Error: ${JSON.stringify(err.message)}`,
);
});
plc.on("TagChanged", async (tag: any, prevVal: any) => { plc.on("TagChanged", async (tag: any, prevVal: any) => {
if (tag.value !== 0) { if (tag.value !== 0) {
const time = nowISO(); const time = nowISO();
if (tag.value === 0) return; if (tag.value === 0) return;
setTimeout(async () => { setTimeout(async () => {
if (tag.value === 0) return; if (tag.value === 0) return;
const macId = await getMac(tag.value); const macId = await getMac(tag.value);
// const { data, error } = (await tryCatch( // const { data, error } = (await tryCatch(
// query( // query(
// getCurrentLabel // getCurrentLabel
// .replace( // .replace(
// "[macId]", // "[macId]",
// macId[0]?.HumanReadableId // macId[0]?.HumanReadableId
// ) // )
// .replace( // .replace(
// "[time]", // "[time]",
// format(time, "yyyy-MM-dd HH:mm") // format(time, "yyyy-MM-dd HH:mm")
// ), // ),
// "Current label data" // "Current label data"
// ) // )
// )) as any; // )) as any;
// createLog( // createLog(
// "info", // "info",
// "zechettii", // "zechettii",
// "zechettii", // "zechettii",
// `${format(time, "yyyy-MM-dd HH:mm")} [${cfg.id}] ${ // `${format(time, "yyyy-MM-dd HH:mm")} [${cfg.id}] ${
// tag.name // tag.name
// }: ${prevVal} -> ${ // }: ${prevVal} -> ${
// tag.value // tag.value
// }, the running number is ${ // }, the running number is ${
// error ? null : data.data[0]?.LfdNr // error ? null : data.data[0]?.LfdNr
// }}` // }}`
// ); // );
const zechette = { const zechette = {
line: tag.value.toString(), line: tag.value.toString(),
printer: cfg.printerId, // this is the id of the zechetti 2 to print we should move this to the db printer: cfg.printerId, // this is the id of the zechetti 2 to print we should move this to the db
printerName: cfg.id, printerName: cfg.id,
}; };
labelingProcess({ zechette: zechette }); createLog(
}, 1000); "info",
} "zechette",
}); "ocp",
}); `Date being sent to labeler: ${JSON.stringify(zechette)}`,
);
labelingProcess({ zechette: zechette });
}, 1000);
}
});
});
isRunning = true; isRunning = true;
}; };
const stop = () => { const stop = () => {
if (!isRunning) return; if (!isRunning) return;
Object.values(controllers).forEach((plc: any) => { Object.values(controllers).forEach((plc: any) => {
try { try {
plc.disconnect(); plc.disconnect();
} catch {} } catch {}
}); });
controllers = {}; controllers = {};
cm = null; cm = null;
isRunning = false; isRunning = false;
console.log("Monitor stopped"); console.log("Monitor stopped");
}; };
const restart = () => { const restart = () => {
console.log("Restarting the plc(s)"); console.log("Restarting the plc(s)");
stop(); stop();
new Promise((resolve) => setTimeout(resolve, 1500)); new Promise((resolve) => setTimeout(resolve, 1500));
start(); start();
}; };
const status = () => { const status = () => {
const result: any = {}; const result: any = {};
for (const [id, s] of Object.entries(stats)) { for (const [id, s] of Object.entries(stats)) {
let s: any; let s: any;
let uptimeMs = null, let uptimeMs = null,
downtimeMs = null; downtimeMs = null;
if (s.connected && s.lastConnectedAt) { if (s.connected && s.lastConnectedAt) {
uptimeMs = Date.now() - new Date(s.lastConnectedAt).getTime(); uptimeMs = Date.now() - new Date(s.lastConnectedAt).getTime();
} else if (!s.connected && s.lastDisconnectedAt) { } else if (!s.connected && s.lastDisconnectedAt) {
downtimeMs = downtimeMs = Date.now() - new Date(s.lastDisconnectedAt).getTime();
Date.now() - new Date(s.lastDisconnectedAt).getTime(); }
} result[id] = { ...s, uptimeMs, downtimeMs };
result[id] = { ...s, uptimeMs, downtimeMs }; }
} return result;
return result; };
};
return { start, stop, restart, status }; return { start, stop, restart, status };
}; };