Compare commits
2 Commits
817a5c6876
...
38edc6214b
| Author | SHA1 | Date | |
|---|---|---|---|
| 38edc6214b | |||
| 2142c06ac3 |
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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(
|
app.use(
|
||||||
basePath + "/api/admin",
|
basePath + "/api/admin",
|
||||||
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
|
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
|
||||||
grantRoles
|
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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
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([
|
||||||
@@ -19,18 +19,25 @@ const roleSchema = z.object({
|
|||||||
"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);
|
||||||
@@ -38,7 +45,7 @@ router.post("/:userId/grant", async (req: Request, res: Response) => {
|
|||||||
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,
|
||||||
})
|
})
|
||||||
@@ -48,7 +55,7 @@ router.post("/:userId/grant", async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
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,
|
||||||
@@ -67,6 +74,7 @@ router.post("/:userId/grant", async (req: Request, res: Response) => {
|
|||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
71
app/src/internal/admin/routes/revokeRole.ts
Normal file
71
app/src/internal/admin/routes/revokeRole.ts
Normal 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;
|
||||||
34
frontend/package-lock.json
generated
34
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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 { ModeToggle } from "../mode-toggle";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -9,8 +11,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import { ModeToggle } from "../mode-toggle";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
|
|
||||||
export default function Nav() {
|
export default function Nav() {
|
||||||
const { session } = useAuth();
|
const { session } = useAuth();
|
||||||
@@ -23,10 +23,7 @@ export default function Nav() {
|
|||||||
</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`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
LST - Docs
|
LST - Docs
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -60,9 +57,7 @@ export default function Nav() {
|
|||||||
<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
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { userAccess } from "../../lib/authClient";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "../ui/sidebar";
|
} from "../ui/sidebar";
|
||||||
import { Header } from "./Header";
|
|
||||||
import Admin from "./Admin";
|
import Admin from "./Admin";
|
||||||
import { userAccess } from "../../lib/authClient";
|
import { Header } from "./Header";
|
||||||
|
|
||||||
export default function SideBarNav() {
|
export default function SideBarNav() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
114
frontend/src/components/ui/table.tsx
Normal file
114
frontend/src/components/ui/table.tsx
Normal 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,
|
||||||
|
}
|
||||||
17
frontend/src/lib/querys/admin/getUsers.ts
Normal file
17
frontend/src/lib/querys/admin/getUsers.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
Outlet,
|
Outlet,
|
||||||
useRouter,
|
useRouter,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
|
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { Toaster } from "sonner";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { SessionGuard } from "../lib/providers/SessionProvider";
|
|
||||||
import Nav from "../components/navBar/Nav";
|
|
||||||
import { ThemeProvider } from "../lib/providers/theme-provider";
|
|
||||||
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 {
|
||||||
@@ -60,6 +59,7 @@ const RootLayout = () => {
|
|||||||
<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">
|
<div className="flex-2 overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
144
frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx
Normal file
144
frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export const createPlcMonitor = (config: any) => {
|
|||||||
cfg.slot,
|
cfg.slot,
|
||||||
cfg.rpi,
|
cfg.rpi,
|
||||||
true,
|
true,
|
||||||
cfg.retrySP || 3000
|
cfg.retrySP || 3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
plc.connect();
|
plc.connect();
|
||||||
@@ -51,18 +52,28 @@ export const createPlcMonitor = (config: any) => {
|
|||||||
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) => {
|
||||||
@@ -104,6 +115,12 @@ export const createPlcMonitor = (config: any) => {
|
|||||||
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,
|
||||||
};
|
};
|
||||||
|
createLog(
|
||||||
|
"info",
|
||||||
|
"zechette",
|
||||||
|
"ocp",
|
||||||
|
`Date being sent to labeler: ${JSON.stringify(zechette)}`,
|
||||||
|
);
|
||||||
labelingProcess({ zechette: zechette });
|
labelingProcess({ zechette: zechette });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -143,8 +160,7 @@ export const createPlcMonitor = (config: any) => {
|
|||||||
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user