Compare commits

..

7 Commits

81 changed files with 9570 additions and 6178 deletions

View File

@@ -5,7 +5,7 @@ meta {
}
get {
url: http://{{url}}/lst/api/me
url: http://{{url}}/lst/api/user/me
body: none
auth: inherit
}

View File

@@ -0,0 +1,24 @@
meta {
name: Get Users
type: http
seq: 7
}
post {
url: http://{{url}}/lst/api/admin/users
body: none
auth: inherit
}
body:json {
{
"username":"matthes01",
"name":"blake",
"email":"blake.matthes@alpla.com",
"password":"nova0511"
}
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,26 @@
meta {
name: GrantROle by ID
type: http
seq: 8
}
post {
url: http://{{url}}/lst/api/admin/:userID/grant
body: json
auth: inherit
}
params:path {
userID: 0hlO48C7Jw1J804FxrCnonK
}
body:json {
{
"module":"users",
"role":"admin"
}
}
settings {
encodeUrl: true
}

View File

@@ -5,16 +5,16 @@ meta {
}
post {
url: http://{{url}}/lst/api/register
url: http://{{url}}/lst/api/user/register
body: json
auth: inherit
}
body:json {
{
"username":"matthes01",
"username":"matthes011",
"name":"blake",
"email":"blake.matthes@alpla.com",
"email":"blake1.matthes@alpla.com",
"password":"nova0511"
}
}

View File

@@ -0,0 +1,35 @@
meta {
name: Session
type: http
seq: 5
}
get {
url: http://{{url}}/lst/api/auth/get-session
body: none
auth: inherit
}
body:json {
{
"username": "matthes01",
"password": "nova0511"
}
}
script:post-response {
// grab the raw Set-Cookie header
const cookies = res.headers["set-cookie"];
// cookies is usually an array, e.g. ["auth_session=abcd123; Path=/; HttpOnly; Secure; SameSite=Lax"]
// Extract just the value part ("auth_session=abcd123")
const sessionCookie = cookies[0].split(";")[0];
// Save it as an environment variable
bru.setEnvVar("session_cookie", sessionCookie);
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,28 @@
meta {
name: Signout
type: http
seq: 6
}
post {
url: http://{{url}}/lst/api/auth/sign-out
body: none
auth: inherit
}
body:json {
{
"username": "matthes01",
"password": "nova0511"
}
}
script:post-response {
// Save it as an environment variable
bru.setEnvVar("session_cookie", null);
}
settings {
encodeUrl: true
}

View File

@@ -78,8 +78,9 @@ const main = async () => {
app.use(express.json());
const allowedOrigins = [
"http://localhost:5173", // dev
"http://localhost:4200",
"http://localhost:5173", // lstV2 dev
"http://localhost:5500", // lst dev
"http://localhost:4200", // express
env.BETTER_AUTH_URL, // prod
];

View File

@@ -0,0 +1,75 @@
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";
export const systemAdminRole = async (userId: string) => {
const log = createLogger({
module: "admin",
subModule: "systemAdminSetup",
});
const systemAdminRoles = [
{
userId: userId,
module: "users",
role: "systemAdmin",
},
{
userId: userId,
module: "system",
role: "systemAdmin",
},
{
userId: userId,
module: "ocp",
role: "systemAdmin",
},
{
userId: userId,
module: "siloAdjustments",
role: "systemAdmin",
},
{
userId: userId,
module: "demandManagement",
role: "systemAdmin",
},
{
userId: userId,
module: "logistics",
role: "systemAdmin",
},
{
userId: userId,
module: "production",
role: "systemAdmin",
},
{
userId: userId,
module: "quality",
role: "systemAdmin",
},
{
userId: userId,
module: "eom",
role: "systemAdmin",
},
{
userId: userId,
module: "forklifts",
role: "systemAdmin",
},
];
const { data, error } = await tryCatch(
db.insert(userRoles).values(systemAdminRoles).onConflictDoNothing()
);
if (error) {
log.error(
{ stack: { error: error } },
"There was an error creating the system admin roles"
);
}
log.info({ data }, "New system admin roles created");
};

View File

@@ -0,0 +1,19 @@
import type { Express, Request, Response } from "express";
import { requireAuth } from "../../pkg/middleware/authMiddleware.js";
//admin routes
import users from "./routes/getUserRoles.js";
import grantRoles from "./routes/grantRole.js";
export const setupAdminRoutes = (app: Express, basePath: string) => {
app.use(
basePath + "/api/admin/users",
requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this
users
);
app.use(
basePath + "/api/admin",
requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this
grantRoles
);
};

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Request, Response } from "express";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
import { db } from "../../../pkg/db/db.js";
import { user } from "../../../pkg/db/schema/auth-schema.js";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
const router = Router();
router.post("/", async (req: Request, res: Response) => {
// should get all users
const { data: users, error: userError } = await tryCatch(
db.select().from(user)
);
if (userError) {
return res.status(500).json({
success: false,
message: "Failed to get users",
error: userError,
});
}
// should get all roles
const { data: userRole, error: userRoleError } = await tryCatch(
db.select().from(userRoles)
);
if (userRoleError) {
return res.status(500).json({
success: false,
message: "Failed to get userRoless",
error: userRoleError,
});
}
// add the roles and return
const usersWithRoles = users.map((user) => {
const roles = userRole
.filter((ur) => ur.userId === user.id)
.map((ur) => ({ module: ur.module, role: ur.role }));
return { ...user, roles };
});
return res
.status(200)
.json({ success: true, message: "User data", data: usersWithRoles });
});
export default router;

View File

@@ -0,0 +1,74 @@
import { Router } from "express";
import type { Request, Response } from "express";
import { tryCatch } from "../../../pkg/utils/tryCatch.js";
import { db } from "../../../pkg/db/db.js";
import z from "zod";
import { userRoles } from "../../../pkg/db/schema/user_roles.js";
import { createLogger } from "../../../pkg/logger/logger.js";
const roleSchema = z.object({
module: z.enum([
"users",
"system",
"ocp",
"siloAdjustments",
"demandManagement",
"logistics",
"production",
"quality",
"eom",
"forklifts",
]),
role: z.enum(["admin", "manager", "supervisor", "test,", "viewer"]),
});
const router = Router();
router.post("/:userId/grant", async (req: Request, res: Response) => {
const log = createLogger({
module: "admin",
subModule: "grantRoles",
});
const userId = req.params.userId;
console.log(userId);
try {
const validated = roleSchema.parse(req.body);
const data = await db
.insert(userRoles)
.values({
userId,
module: validated.module,
role: validated.role,
})
.onConflictDoUpdate({
target: [userRoles.userId, userRoles.module],
set: { module: validated.module, role: validated.role },
});
log.info(
{},
`Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}`
);
return res.status(200).json({
success: true,
message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`,
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.",
});
}
});
export default router;

View File

@@ -1,11 +1,12 @@
import { Router } from "express";
import { Router, type Request, type Response } from "express";
import { auth } from "../../../pkg/auth/auth.js";
import { fromNodeHeaders } from "better-auth/node";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
const router = Router();
// GET /health
router.get("/", async (req, res) => {
router.get("/", async (req: Request, res: Response) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

View File

@@ -2,6 +2,11 @@ import { Router } from "express";
import type { Request, Response } from "express";
import z from "zod";
import { auth } from "../../../pkg/auth/auth.js";
import { db } from "../../../pkg/db/db.js";
import { count } from "drizzle-orm";
import { user } from "../../../pkg/db/schema/auth-schema.js";
import { APIError, betterAuth } from "better-auth";
import { systemAdminRole } from "../../admin/controller/systemAdminRole.js";
const router = Router();
@@ -18,6 +23,9 @@ const registerSchema = z.object({
});
router.post("/", async (req: Request, res: Response) => {
// check if we are the first user so we can add as system admin to all modules
const totalUsers = await db.select({ count: count() }).from(user);
console.log(totalUsers[0].count);
try {
// Parse + validate incoming JSON against Zod schema
const validated = registerSchema.parse(req.body);
@@ -27,6 +35,9 @@ router.post("/", async (req: Request, res: Response) => {
body: validated,
});
if (totalUsers[0].count === 0) {
systemAdminRole(user.user.id);
}
return res.status(201).json(user);
} catch (err) {
if (err instanceof z.ZodError) {
@@ -36,8 +47,14 @@ router.post("/", async (req: Request, res: Response) => {
details: flattened,
});
}
console.error(err);
return res.status(500).json({ error: "Internal server error" });
if (err instanceof APIError) {
return res.status(400).json({
success: false,
message: err.message,
error: err.status,
});
}
}
});

View File

@@ -1,8 +1,9 @@
import type { Express, Request, Response } from "express";
import me from "./me.js";
import register from "./register.js";
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
export const setupAuthRoutes = (app: Express, basePath: string) => {
app.use(basePath + "/api/me", me);
app.use(basePath + "/api/auth/register", register);
app.use(basePath + "/api/user/me", requireAuth(), me);
app.use(basePath + "/api/user/register", register);
};

View File

@@ -2,6 +2,7 @@ import type { Express, Request, Response } from "express";
import healthRoutes from "./routes/healthRoutes.js";
import { setupAuthRoutes } from "../auth/routes/routes.js";
import { setupAdminRoutes } from "../admin/routes.js";
export const setupRoutes = (app: Express, basePath: string) => {
// Root / health check
@@ -9,6 +10,7 @@ export const setupRoutes = (app: Express, basePath: string) => {
// all routes
setupAuthRoutes(app, basePath);
setupAdminRoutes(app, basePath);
// always try to go to the app weather we are in dev or in production.
app.get(basePath + "/", (req: Request, res: Response) => {

View File

@@ -20,20 +20,26 @@ export const auth = betterAuth({
provider: "pg",
schema,
}),
trustedOrigins: [
"*.alpla.net",
"http://localhost:5173",
"http://localhost:5500",
],
appName: "lst",
emailAndPassword: {
enabled: true,
minPasswordLength: 8, // optional config
},
plugins: [
jwt({ jwt: { expirationTime: "1h" } }),
//jwt({ jwt: { expirationTime: "1h" } }),
apiKey(),
admin(),
username(),
],
session: {
expiresIn: 60 * 60,
updateAge: 60 * 1,
updateAge: 60 * 5,
freshAge: 60 * 2,
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache duration in seconds

View File

@@ -1,8 +1,8 @@
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
import { user } from "./auth-schema.js";
export const userRole = pgTable(
"user_role",
export const userRoles = pgTable(
"user_roles",
{
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
userId: text("user_id")
@@ -15,3 +15,4 @@ export const userRole = pgTable(
uniqueIndex("unique_user_module").on(table.userId, table.module),
]
);
export type UserRole = typeof userRoles.$inferSelect;

View File

@@ -0,0 +1,90 @@
import type { Request, Response, NextFunction } from "express";
import { auth } from "../auth/auth.js";
import { userRoles, type UserRole } from "../db/schema/user_roles.js";
import { db } from "../db/db.js";
import { eq } from "drizzle-orm";
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email?: string;
roles: Record<string, string[]>;
};
}
}
}
function toWebHeaders(nodeHeaders: Request["headers"]): Headers {
const h = new Headers();
for (const [key, value] of Object.entries(nodeHeaders)) {
if (Array.isArray(value)) {
value.forEach((v) => h.append(key, v));
} else if (value !== undefined) {
h.set(key, value);
}
}
return h;
}
export const requireAuth = (moduleName?: string, requiredRoles?: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const headers = toWebHeaders(req.headers);
// Get session
const session = await auth.api.getSession({
headers,
query: { disableCookieCache: true },
});
if (!session) {
return res.status(401).json({ error: "No active session" });
}
const userId = session.user.id;
// Get roles
const roles = await db
.select()
.from(userRoles)
.where(eq(userRoles.userId, userId));
// Organize roles by module
const rolesByModule: Record<string, string[]> = {};
for (const r of roles) {
if (!rolesByModule[r.module]) rolesByModule[r.module] = [];
rolesByModule[r.module].push(r.role);
}
req.user = {
id: userId,
email: session.user.email,
roles: rolesByModule,
};
// SystemAdmin override
const hasSystemAdmin = Object.values(rolesByModule)
.flat()
.includes("systemAdmin");
// Role check (skip if systemAdmin)
if (requiredRoles?.length && !hasSystemAdmin) {
const moduleRoles = moduleName
? rolesByModule[moduleName] ?? []
: Object.values(rolesByModule).flat();
const hasAccess = moduleRoles.some((role) =>
requiredRoles.includes(role)
);
if (!hasAccess) {
return res.status(403).json({ error: "Forbidden" });
}
}
next();
} catch (err) {
console.error("Auth middleware error:", err);
res.status(500).json({ error: "Auth check failed" });
}
};
};

12080
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,50 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-router": "^1.131.36",
"@tanstack/react-router-devtools": "^1.131.36",
"better-auth": "^1.3.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tanstack/router-plugin": "^1.131.36",
"@types/node": "^24.3.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react-swc": "^4.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.8",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-form": "^1.23.0",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-router": "^1.131.36",
"@tanstack/react-router-devtools": "^1.131.36",
"axios": "^1.12.2",
"better-auth": "^1.3.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tanstack/router-plugin": "^1.131.36",
"@types/node": "^24.3.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react-swc": "^4.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.8",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}

View File

@@ -0,0 +1,29 @@
import { Link } from "@tanstack/react-router";
import { useAuth, useLogout } from "../../lib/authClient";
import { Button } from "../ui/button";
export default function Nav() {
const { session } = useAuth();
const logout = useLogout();
return (
<nav>
{session?.session ? (
<>
<Button
onClick={() => {
logout();
}}
>
Sign Out
</Button>
</>
) : (
<>
<Button>
<Link to="/login">Login</Link>
</Button>
</>
)}
</nav>
);
}

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,23 @@
import type { ReactNode } from "react";
import { Card } from "../ui/card";
interface LstCardProps {
children?: ReactNode;
className?: string;
style?: React.CSSProperties;
}
export function LstCard({
children,
className = "",
style = {},
}: LstCardProps) {
return (
<Card
className={`border-solid border-1 border-[#00659c] ${className}`}
style={style}
>
{children}
</Card>
);
}

View File

@@ -0,0 +1,183 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -1,11 +1,139 @@
import { createAuthClient } from "better-auth/client";
import { usernameClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`, // 👈 This is fine
callbacks: {
onUpdate: (session: any) => console.log("Session updated", session),
onSignIn: (session: any) => console.log("Signed in!", session),
onSignOut: () => console.log("Signed out!"),
import { create } from "zustand";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { useRouter } from "@tanstack/react-router";
// ---- TYPES ----
export type Session = typeof authClient.$Infer.Session | null;
// Zustand store type
type SessionState = {
session: Session;
setSession: (session: Session) => void;
clearSession: () => void;
};
export type UserRoles = {
userRoleId: string;
userId: string;
module: string;
role: "systemAdmin" | "admin" | "manager" | "user" | "viewer";
};
type UserRoleState = {
userRoles: UserRoles[] | null;
fetchRoles: (userId: string) => Promise<void>;
clearRoles: () => void;
};
// ---- ZUSTAND STORE ----
export const useAuth = create<SessionState>((set) => ({
session: null,
setSession: (session) => set({ session }),
clearSession: () => set({ session: null }),
}));
export const useUserRoles = create<UserRoleState>((set) => ({
userRoles: null,
fetchRoles: async (userId: string) => {
try {
const res = await fetch(`/api/${userId}/roles`, {
credentials: "include",
});
if (!res.ok) throw new Error("Failed to fetch roles");
const roles = await res.json();
set({ userRoles: roles });
} catch (err) {
console.error("Error fetching roles:", err);
set({ userRoles: null });
}
},
clearRoles: () => set({ userRoles: null }),
}));
// ---- BETTER AUTH CLIENT ----
export const authClient = createAuthClient({
baseURL: `${window.location.origin}/lst/api/auth`,
plugins: [usernameClient()],
callbacks: {
callbacks: {
onUpdate: (res: any) => {
// res has strong type
// res.data is `Session | null`
useAuth.getState().setSession(res?.data ?? null);
},
onSignIn: (res: any) => {
console.log("Setting session to ", res?.data);
useAuth.getState().setSession(res?.data ?? null);
},
onSignOut: () => {
useAuth.getState().clearSession();
},
},
},
});
// ---- AUTH API HELPERS ----
export async function signin(data: { username: string; password: string }) {
const res = await authClient.signIn.username(data);
if (res.error) throw res.error;
await authClient.getSession();
return res.data;
}
export const useLogout = () => {
const { clearSession } = useAuth();
const router = useRouter();
const logout = async () => {
await authClient.signOut();
router.invalidate();
router.clearCache();
clearSession();
window.location.reload();
};
return logout;
};
export async function getSession() {
const res = await authClient.getSession({
query: { disableCookieCache: true },
});
if (res.error) return null;
return res.data;
}
// ---- REACT QUERY INTEGRATION ----
export function useSession() {
const { setSession, clearSession } = useAuth();
const qc = useQueryClient();
const query = useQuery({
queryKey: ["session"],
queryFn: getSession,
refetchInterval: 60_000,
refetchOnWindowFocus: true,
});
//console.log("Auth Check", query.data);
// react to data change
useEffect(() => {
if (query.data !== undefined) {
setSession(query.data);
}
}, [query.data, setSession]);
// react to error
useEffect(() => {
if (query.error) {
clearSession();
qc.removeQueries({ queryKey: ["session"] });
}
}, [query.error, qc, clearSession]);
return query;
}

View File

@@ -0,0 +1,34 @@
import { Label } from "@radix-ui/react-label";
import { useFieldContext } from "..";
import { FieldErrors } from "./FieldErrors";
import { Checkbox } from "../../../components/ui/checkbox";
type CheckboxFieldProps = {
label: string;
description?: string;
};
export const CheckboxField = ({ label }: CheckboxFieldProps) => {
const field = useFieldContext<boolean>();
return (
<div>
<div className="m-2 p-2 flex flex-row">
<div>
<Label htmlFor="active">
<span>{label}</span>
</Label>
</div>
<Checkbox
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) => {
field.handleChange(checked === true);
}}
onBlur={field.handleBlur}
/>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,16 @@
import type { AnyFieldMeta } from "@tanstack/react-form";
import { ZodError } from "zod";
type FieldErrorsProps = {
meta: AnyFieldMeta;
};
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
if (!meta.isTouched) return null;
return meta.errors.map(({ message }: ZodError, index) => (
<p key={index} className="text-sm font-medium text-destructive">
{message}
</p>
));
};

View File

@@ -0,0 +1,28 @@
import { useFieldContext } from "..";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { FieldErrors } from "./FieldErrors";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
const field = useFieldContext<any>();
return (
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Input
id={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
type={inputType}
required={required}
/>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { useFieldContext } from "..";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../components/ui/select";
import { FieldErrors } from "./FieldErrors";
import { Label } from "../../../components/ui/label";
type SelectOption = {
value: string;
label: string;
};
type SelectFieldProps = {
label: string;
options: SelectOption[];
placeholder?: string;
};
export const SelectField = ({
label,
options,
placeholder,
}: SelectFieldProps) => {
const field = useFieldContext<string>();
return (
<div className="grid gap-3">
<div className="grid gap-3">
<Label htmlFor={field.name}>{label}</Label>
<Select
value={field.state.value}
onValueChange={(value) => field.handleChange(value)}
>
<SelectTrigger
id={field.name}
onBlur={field.handleBlur}
className="w-[380px]"
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<FieldErrors meta={field.state.meta} />
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { useStore } from "@tanstack/react-form";
import { useFormContext } from "..";
import { Button } from "../../../components/ui/button";
type SubmitButtonProps = {
children: React.ReactNode;
};
export const SubmitButton = ({ children }: SubmitButtonProps) => {
const form = useFormContext();
const [isSubmitting] = useStore(form.store, (state) => [
state.isSubmitting,
state.canSubmit,
]);
return (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{children}
</Button>
</div>
);
};

View File

@@ -0,0 +1,15 @@
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
import { SubmitButton } from "./components/SubmitButton";
import { InputField } from "./components/InputField";
import { SelectField } from "./components/SelectField";
import { CheckboxField } from "./components/CheckBox";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
export const { useAppForm } = createFormHook({
fieldComponents: { InputField, SelectField, CheckboxField },
formComponents: { SubmitButton },
fieldContext,
formContext,
});

View File

@@ -0,0 +1,18 @@
import { useRouter } from "@tanstack/react-router";
import { useEffect } from "react";
import { useSession } from "../authClient";
export function SessionGuard({ children }: { children: React.ReactNode }) {
const { data: session, isLoading } = useSession();
const router = useRouter();
useEffect(() => {
if (!isLoading && !session) {
router.navigate({ to: "/" }); // redirect if not logged in
}
}, [isLoading, session, router]);
if (isLoading) return <div>Checking session</div>;
return <>{children}</>;
}

View File

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

View File

@@ -3,15 +3,29 @@ import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";
// Create a client
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 0,
refetchOnWindowFocus: false,
},
},
});
// Create a new router instance
const router = createRouter({ routeTree, basepath: "/lst/app" });
const router = createRouter({
routeTree,
basepath: "/lst/app",
context: {
queryClient: {} as QueryClient,
//login: () => {},
//logout: () => {},
},
});
// Register the router instance for type safety
declare module "@tanstack/react-router" {
@@ -20,10 +34,18 @@ declare module "@tanstack/react-router" {
}
}
const App = () => {
return (
<>
<RouterProvider router={router} context={{ queryClient }} />
</>
);
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<App />
</QueryClientProvider>
</StrictMode>
);

View File

@@ -9,160 +9,48 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as _adminRouteImport } from './routes/__admin'
import { Route as AdminLayoutRouteRouteImport } from './routes/_adminLayout/route'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AdminSettingsRouteImport } from './routes/admin_/settings'
import { Route as AdminLayoutAdminIndexRouteImport } from './routes/_adminLayout/admin/index'
import { Route as AdminLayoutAdminUsersRouteImport } from './routes/_adminLayout/admin/users'
import { Route as AdminLayoutAdminServersRouteImport } from './routes/_adminLayout/admin/servers'
import { Route as AdminLayoutAdminUserUserIdRouteImport } from './routes/_adminLayout/admin/user/$userId'
import { Route as AdminLayoutAdminServersServerIdRouteImport } from './routes/_adminLayout/admin/servers/$serverId'
import { Route as AdminLayoutAdminServersServerIdEditRouteImport } from './routes/_adminLayout/admin/servers/$serverId_/edit'
import { Route as authLoginRouteImport } from './routes/(auth)/login'
const _adminRoute = _adminRouteImport.update({
id: '/__admin',
getParentRoute: () => rootRouteImport,
} as any)
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
id: '/_adminLayout',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AdminSettingsRoute = AdminSettingsRouteImport.update({
id: '/admin_/settings',
path: '/admin/settings',
const authLoginRoute = authLoginRouteImport.update({
id: '/(auth)/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AdminLayoutAdminIndexRoute = AdminLayoutAdminIndexRouteImport.update({
id: '/admin/',
path: '/admin/',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminUsersRoute = AdminLayoutAdminUsersRouteImport.update({
id: '/admin/users',
path: '/admin/users',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminServersRoute = AdminLayoutAdminServersRouteImport.update({
id: '/admin/servers',
path: '/admin/servers',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminUserUserIdRoute =
AdminLayoutAdminUserUserIdRouteImport.update({
id: '/admin/user/$userId',
path: '/admin/user/$userId',
getParentRoute: () => AdminLayoutRouteRoute,
} as any)
const AdminLayoutAdminServersServerIdRoute =
AdminLayoutAdminServersServerIdRouteImport.update({
id: '/$serverId',
path: '/$serverId',
getParentRoute: () => AdminLayoutAdminServersRoute,
} as any)
const AdminLayoutAdminServersServerIdEditRoute =
AdminLayoutAdminServersServerIdEditRouteImport.update({
id: '/$serverId_/edit',
path: '/$serverId/edit',
getParentRoute: () => AdminLayoutAdminServersRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/admin/users': typeof AdminLayoutAdminUsersRoute
'/admin': typeof AdminLayoutAdminIndexRoute
'/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/admin/servers/$serverId/edit': typeof AdminLayoutAdminServersServerIdEditRoute
'/login': typeof authLoginRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/admin/users': typeof AdminLayoutAdminUsersRoute
'/admin': typeof AdminLayoutAdminIndexRoute
'/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/admin/servers/$serverId/edit': typeof AdminLayoutAdminServersServerIdEditRoute
'/login': typeof authLoginRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
'/__admin': typeof _adminRoute
'/admin_/settings': typeof AdminSettingsRoute
'/_adminLayout/admin/servers': typeof AdminLayoutAdminServersRouteWithChildren
'/_adminLayout/admin/users': typeof AdminLayoutAdminUsersRoute
'/_adminLayout/admin/': typeof AdminLayoutAdminIndexRoute
'/_adminLayout/admin/servers/$serverId': typeof AdminLayoutAdminServersServerIdRoute
'/_adminLayout/admin/user/$userId': typeof AdminLayoutAdminUserUserIdRoute
'/_adminLayout/admin/servers/$serverId_/edit': typeof AdminLayoutAdminServersServerIdEditRoute
'/(auth)/login': typeof authLoginRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/admin/settings'
| '/admin/servers'
| '/admin/users'
| '/admin'
| '/admin/servers/$serverId'
| '/admin/user/$userId'
| '/admin/servers/$serverId/edit'
fullPaths: '/' | '/login'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/admin/settings'
| '/admin/servers'
| '/admin/users'
| '/admin'
| '/admin/servers/$serverId'
| '/admin/user/$userId'
| '/admin/servers/$serverId/edit'
id:
| '__root__'
| '/'
| '/_adminLayout'
| '/__admin'
| '/admin_/settings'
| '/_adminLayout/admin/servers'
| '/_adminLayout/admin/users'
| '/_adminLayout/admin/'
| '/_adminLayout/admin/servers/$serverId'
| '/_adminLayout/admin/user/$userId'
| '/_adminLayout/admin/servers/$serverId_/edit'
to: '/' | '/login'
id: '__root__' | '/' | '/(auth)/login'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
_adminRoute: typeof _adminRoute
AdminSettingsRoute: typeof AdminSettingsRoute
authLoginRoute: typeof authLoginRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/__admin': {
id: '/__admin'
path: ''
fullPath: ''
preLoaderRoute: typeof _adminRouteImport
parentRoute: typeof rootRouteImport
}
'/_adminLayout': {
id: '/_adminLayout'
path: ''
fullPath: ''
preLoaderRoute: typeof AdminLayoutRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -170,97 +58,19 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin_/settings': {
id: '/admin_/settings'
path: '/admin/settings'
fullPath: '/admin/settings'
preLoaderRoute: typeof AdminSettingsRouteImport
'/(auth)/login': {
id: '/(auth)/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof authLoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_adminLayout/admin/': {
id: '/_adminLayout/admin/'
path: '/admin'
fullPath: '/admin'
preLoaderRoute: typeof AdminLayoutAdminIndexRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/users': {
id: '/_adminLayout/admin/users'
path: '/admin/users'
fullPath: '/admin/users'
preLoaderRoute: typeof AdminLayoutAdminUsersRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/servers': {
id: '/_adminLayout/admin/servers'
path: '/admin/servers'
fullPath: '/admin/servers'
preLoaderRoute: typeof AdminLayoutAdminServersRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/user/$userId': {
id: '/_adminLayout/admin/user/$userId'
path: '/admin/user/$userId'
fullPath: '/admin/user/$userId'
preLoaderRoute: typeof AdminLayoutAdminUserUserIdRouteImport
parentRoute: typeof AdminLayoutRouteRoute
}
'/_adminLayout/admin/servers/$serverId': {
id: '/_adminLayout/admin/servers/$serverId'
path: '/$serverId'
fullPath: '/admin/servers/$serverId'
preLoaderRoute: typeof AdminLayoutAdminServersServerIdRouteImport
parentRoute: typeof AdminLayoutAdminServersRoute
}
'/_adminLayout/admin/servers/$serverId_/edit': {
id: '/_adminLayout/admin/servers/$serverId_/edit'
path: '/$serverId/edit'
fullPath: '/admin/servers/$serverId/edit'
preLoaderRoute: typeof AdminLayoutAdminServersServerIdEditRouteImport
parentRoute: typeof AdminLayoutAdminServersRoute
}
}
}
interface AdminLayoutAdminServersRouteChildren {
AdminLayoutAdminServersServerIdRoute: typeof AdminLayoutAdminServersServerIdRoute
AdminLayoutAdminServersServerIdEditRoute: typeof AdminLayoutAdminServersServerIdEditRoute
}
const AdminLayoutAdminServersRouteChildren: AdminLayoutAdminServersRouteChildren =
{
AdminLayoutAdminServersServerIdRoute: AdminLayoutAdminServersServerIdRoute,
AdminLayoutAdminServersServerIdEditRoute:
AdminLayoutAdminServersServerIdEditRoute,
}
const AdminLayoutAdminServersRouteWithChildren =
AdminLayoutAdminServersRoute._addFileChildren(
AdminLayoutAdminServersRouteChildren,
)
interface AdminLayoutRouteRouteChildren {
AdminLayoutAdminServersRoute: typeof AdminLayoutAdminServersRouteWithChildren
AdminLayoutAdminUsersRoute: typeof AdminLayoutAdminUsersRoute
AdminLayoutAdminIndexRoute: typeof AdminLayoutAdminIndexRoute
AdminLayoutAdminUserUserIdRoute: typeof AdminLayoutAdminUserUserIdRoute
}
const AdminLayoutRouteRouteChildren: AdminLayoutRouteRouteChildren = {
AdminLayoutAdminServersRoute: AdminLayoutAdminServersRouteWithChildren,
AdminLayoutAdminUsersRoute: AdminLayoutAdminUsersRoute,
AdminLayoutAdminIndexRoute: AdminLayoutAdminIndexRoute,
AdminLayoutAdminUserUserIdRoute: AdminLayoutAdminUserUserIdRoute,
}
const AdminLayoutRouteRouteWithChildren =
AdminLayoutRouteRoute._addFileChildren(AdminLayoutRouteRouteChildren)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
_adminRoute: _adminRoute,
AdminSettingsRoute: AdminSettingsRoute,
authLoginRoute: authLoginRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -0,0 +1,75 @@
import { useRouter, useSearch } from "@tanstack/react-router";
import { getSession, signin, useAuth } from "../../../lib/authClient";
import { useAppForm } from "../../../lib/formStuff";
import { LstCard } from "../../../components/ui/lstCard";
import { toast } from "sonner";
export default function LoginForm() {
const router = useRouter();
const search = useSearch({ from: "/(auth)/login" });
const { setSession } = useAuth();
const form = useAppForm({
defaultValues: {
username: "",
password: "",
},
onSubmit: async ({ value }) => {
try {
await signin({
username: value.username,
password: value.password,
});
const session = await getSession();
setSession(session);
toast.success(
`Welcome back ${session?.user.name ? session?.user.name : session?.user.username} `
);
router.invalidate();
router.history.push(search.redirect ? search.redirect : "/");
} catch (error) {
console.log(error);
}
},
});
return (
<div className="ml-[25%]">
<LstCard className="p-3 w-96">
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField
name="username"
children={(field) => (
<field.InputField
label="Username"
inputType="string"
required={true}
/>
)}
/>
<form.AppField
name="password"
children={(field) => (
<field.InputField
label="Password"
inputType="password"
required={true}
/>
)}
/>
<div className="flex justify-end mt-2">
<form.AppForm>
<form.SubmitButton>Login</form.SubmitButton>
</form.AppForm>
</div>
</form>
</LstCard>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { z } from "zod";
import { authClient } from "../../lib/authClient";
import LoginForm from "./-components/LoginForm";
export const Route = createFileRoute("/(auth)/login")({
component: RouteComponent,
validateSearch: z.object({
redirect: z.string().optional(),
}),
beforeLoad: async () => {
const result = await authClient.getSession({
query: { disableCookieCache: true }, // force DB/Server lookup
});
//console.log("session check:", result.data);
if (result.data) {
throw redirect({ to: "/" });
}
},
});
function RouteComponent() {
return (
<div>
<LoginForm />
</div>
);
}

View File

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

View File

@@ -1,16 +1,32 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import type { QueryClient } from "@tanstack/react-query";
import { Toaster } from "sonner";
import { SessionGuard } from "../lib/providers/SessionProvider";
import Nav from "../components/navBar/Nav";
const RootLayout = () => (
<>
<nav className="flex gap-1">
<Link to="/">Home</Link>
{/* <Link to="/admin">Admin</Link> */}
</nav>
<hr></hr>
<Outlet />
<TanStackRouterDevtools />
</>
);
interface RootRouteContext {
queryClient: QueryClient;
//user: User | null;
//login: (user: User) => void;
//logout: () => void;
}
export const Route = createRootRoute({ component: RootLayout });
const RootLayout = () => {
//const { logout, login } = Route.useRouteContext();
return (
<div>
<SessionGuard>
<Nav />
<Outlet />
<Toaster expand={true} richColors closeButton />
<TanStackRouterDevtools />
</SessionGuard>
</div>
);
};
export const Route = createRootRouteWithContext<RootRouteContext>()({
component: RootLayout,
});

View File

@@ -1,23 +0,0 @@
import { Link } from "@tanstack/react-router";
export default function Nav() {
return (
<div>
<nav className="flex gap-1">
<Link
to="/admin"
className="[&.active]:font-bold"
activeOptions={{ exact: true }}
>
Admin
</Link>
<Link to="/admin/servers" className="[&.active]:font-bold">
Servers
</Link>
<Link to="/admin/users" className="[&.active]:font-bold">
Users
</Link>
</nav>
</div>
);
}

View File

@@ -1,8 +0,0 @@
import { getRouteApi } from "@tanstack/react-router";
export default function Server() {
const { useParams } = getRouteApi("/_adminLayout/admin/servers/$serverId");
const { serverId } = useParams();
return <div>server id {serverId}!</div>;
}

View File

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

View File

@@ -1,27 +0,0 @@
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/_adminLayout/admin/servers")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
Hello "/admin/servers"! <br />
<Link
to="/admin/servers/$serverId"
params={{
serverId: "5",
}}
>
Server 5
</Link>
<Link
to="/admin/servers/$serverId"
params={(prev) => ({ ...prev, serverId: "10" })}
>
Server 5
</Link>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import Server from "../../-components/Server";
export const Route = createFileRoute("/_adminLayout/admin/servers/$serverId")({
component: RouteComponent,
});
function RouteComponent() {
return <Server />;
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import Nav from "./-components/Nav";
export const Route = createFileRoute("/_adminLayout")({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
<Nav />
<hr />
<Outlet />
</div>
);
}

View File

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

View File

@@ -1,19 +1,24 @@
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "../components/ui/button";
import { useAuth } from "../lib/authClient";
export const Route = createFileRoute("/")({
component: Index,
});
function Index() {
const { session } = useAuth();
return (
<div>
<div className="h-screen flex items-center justify-center">
<Button className="h-96 w-96">
<a href="/lst/d" target="_blank" className="text-4xl">
LST-DOCS
</a>
</Button>
<div className="h-screen flex flex-col items-center justify-center">
<div>Welcome, {session ? session.user.username : "Guest"}</div>
<div>
<Button className="h-96 w-96">
<a href="/lst/d" target="_blank" className="text-4xl">
LST-DOCS
</a>
</Button>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,6 @@
export type User = {
id?: number;
name?: string;
username: string;
password: string;
};

View File

@@ -35,6 +35,7 @@ export default defineConfig({
},
},
server: {
port: 5500,
proxy: {
"/lst/api": {
target: `http://localhost:${Number(

View File

@@ -23,6 +23,7 @@ const LoginForm = () => {
const username = localStorage.getItem("username") || "";
const router = useRouter();
const search = useSearch({ from: "/login" });
const {
register,
handleSubmit,

View File

@@ -92,7 +92,7 @@ export default function ManualPrintForm() {
};
const onSubmit = (data: any) => {
//console.log(data);
console.log(data);
handleManualPrintLog(data);
};
@@ -243,8 +243,10 @@ export default function ManualPrintForm() {
<Textarea
//label="Comments"
placeholder="add more info as needed."
{...(register("additionalComments"),
{ required: true, minLength: 10 })}
{
...register("additionalComments")
// { required: true, minLength: 10 }
}
/>
</div>

View File

@@ -20,17 +20,10 @@ export const SessionProvider = ({
useEffect(() => {
fetchModules();
fetchSettings();
console.log("settings grab ran");
fetchUserRoles();
fetchSubModules();
}, []);
//temp
localStorage.removeItem("ally-supports-cache");
localStorage.removeItem("auth-storage");
localStorage.removeItem("nextauth.message");
localStorage.removeItem("prod");
localStorage.removeItem("card-storage");
return (
<QueryClientProvider client={queryClient}>
{children}

View File

@@ -71,7 +71,7 @@ export const Route = createRootRoute({
href={`https://${server[0].value}.alpla.net/lst/d`}
target="_blank"
>
LST - Docs
LST - Docs |
</a>
)}
</div>

View File

@@ -8,7 +8,7 @@
"dev:frontend": "cd frontend && npm run dev",
"dev:dbgen": " drizzle-kit generate --config=drizzle-dev.config.ts",
"dev:dbmigrate": " drizzle-kit migrate --config=drizzle-dev.config.ts",
"build": "npm run build:server && npm run build:frontend && npm run zipServer",
"build": "npm run build:server && npm run build:frontend",
"build:server": "rimraf dist && tsc --build && npm run copy:scripts && xcopy server\\services\\notifications\\utils\\views\\ dist\\server\\services\\notifications\\utils\\views\\ /E /I /Y",
"build:frontend": "cd frontend && npm run build",
"build:iisNet": "rimraf dotnetwrapper\\bin && xcopy frontend\\dist dotnetwrapper\\wwwroot /E /I /Y && cd dotnetwrapper && dotnet publish lst-wrapper.csproj --configuration Release --output ../prodBuild",

View File

@@ -1,22 +1,27 @@
import jwt from "jsonwebtoken";
import {db} from "../../../../database/dbclient.js";
import {users} from "../../../../database/schema/users.js";
import {eq, sql} from "drizzle-orm";
import {checkPassword} from "../utils/checkPassword.js";
import {roleCheck} from "./userRoles/getUserAccess.js";
import {createLog} from "../../logger/logger.js";
import {differenceInDays} from "date-fns";
import { db } from "../../../../database/dbclient.js";
import { users } from "../../../../database/schema/users.js";
import { eq, sql } from "drizzle-orm";
import { checkPassword } from "../utils/checkPassword.js";
import { roleCheck } from "./userRoles/getUserAccess.js";
import { createLog } from "../../logger/logger.js";
import { differenceInDays } from "date-fns";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { settings } from "../../../../database/schema/settings.js";
/**
* Authenticate a user and return a JWT.
*/
const {sign} = jwt;
const { sign } = jwt;
export async function login(
username: string,
password: string
): Promise<{token: string; user: {user_id: string; username: string}}> {
const user = await db.select().from(users).where(eq(users.username, username));
): Promise<{ token: string; user: { user_id: string; username: string } }> {
const user = await db
.select()
.from(users)
.where(eq(users.username, username));
//console.log(user);
if (user.length === 0) {
@@ -47,24 +52,46 @@ export async function login(
// update the user last login
try {
const lastLog = await db
.update(users)
.set({lastLogin: sql`NOW()`})
.where(eq(users.user_id, user[0].user_id))
.returning({lastLogin: users.lastLogin});
.update(users)
.set({ lastLogin: sql`NOW()` })
.where(eq(users.user_id, user[0].user_id))
.returning({ lastLogin: users.lastLogin });
createLog(
"info",
"lst",
"auth",
`Its been ${differenceInDays(lastLog[0]?.lastLogin ?? "", new Date(Date.now()))} days since ${
user[0].username
} has logged in`
`Its been ${differenceInDays(
lastLog[0]?.lastLogin ?? "",
new Date(Date.now())
)} days since ${user[0].username} has logged in`
);
//]);
} catch (error) {
createLog("error", "lst", "auth", "There was an error updating the user last login");
createLog(
"error",
"lst",
"auth",
"There was an error updating the user last login"
);
}
const token = sign({user: userData}, secret, {expiresIn: expiresIn * 60});
const token = sign({ user: userData }, secret, {
expiresIn: expiresIn * 60,
});
return {token, user: userData};
return { token, user: userData };
}
// export const login = async (username: string, password: string) => {
// // get the settings so we know what server to call.
// const { data, error } = (await tryCatch(db.select().from(settings))) as any;
// if (error) {
// return {
// success: false,
// message: "Failed to get settings",
// data: error,
// };
// }
// };

View File

@@ -0,0 +1,47 @@
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { createLog } from "../../logger/logger.js";
import { query } from "../../sqlServer/prodSqlServer.js";
import { forecastByAvs } from "../../sqlServer/querys/dataMart/forecast.js";
// type ArticleData = {
// id: string
// }
export const getForecastByAv = async (avs: string) => {
let articles: any = [];
if (!avs) {
return {
success: false,
message: `Missing av's please send at least one over`,
data: [],
};
}
const { data, error } = (await tryCatch(
query(forecastByAvs.replace("[articles]", avs), "ForecastData by av")
)) as any;
if (error) {
createLog(
"error",
"datamart",
"datamart",
`There was an error getting the forecast info: ${JSON.stringify(
error
)}`
);
return {
success: false,
messsage: `There was an error getting the forecast info`,
data: error,
};
}
articles = data.data;
return {
success: true,
message: "Forecast Data",
data: articles,
};
};

View File

@@ -0,0 +1,84 @@
import { query } from "../../sqlServer/prodSqlServer.js";
import { deliveryByDateRangeAndAv } from "../../sqlServer/querys/dataMart/deleveryByDateRange.js";
import { addDays, format } from "date-fns";
export const getDeliveryByDateRangeAndAv = async (
avs: string,
startDate: string,
endDate: string
) => {
// const { data: plantToken, error: plantError } = await tryCatch(
// db.select().from(settings).where(eq(settings.name, "plantToken"))
// );
// if (plantError) {
// return {
// success: false,
// message: "Error getting Settings",
// data: plantError,
// };
// }
let deliverys: any = [];
let updatedQuery = deliveryByDateRangeAndAv;
// start days can be sent over
if (startDate) {
updatedQuery = updatedQuery.replaceAll("[startDate]", startDate);
} else {
updatedQuery = updatedQuery.replaceAll("[startDate]", "1990-1-1");
}
// end days can be sent over
if (endDate) {
updatedQuery = updatedQuery.replaceAll("[endDate]", endDate);
} else {
const defaultEndDate = format(
addDays(new Date(Date.now()), 5),
"yyyy-M-d"
);
updatedQuery = updatedQuery.replaceAll("[endDate]", defaultEndDate);
}
try {
const res: any = await query(
updatedQuery.replace("[articles]", avs),
"Get Delivery by date range"
);
deliverys = res.data;
//console.log(res.data);
} catch (error) {
console.log(error);
return {
success: false,
message: "All Deliveries within the range.",
data: error,
};
}
// if (!data) {
// deliverys = deliverys.splice(1000, 0);
// }
// add plant token in
// const pOrders = deliverys.map((item: any) => {
// // const dateCon = new Date(item.loadingDate).toLocaleString("en-US", {
// // month: "numeric",
// // day: "numeric",
// // year: "numeric",
// // hour: "2-digit",
// // minute: "2-digit",
// // hour12: false,
// // });
// //const dateCon = new Date(item.loadingDate).toISOString().replace("T", " ").split(".")[0];
// const dateCon = new Date(item.loadingDate).toISOString().split("T")[0];
// //const delDate = new Date(item.deliveryDate).toISOString().replace("T", " ").split(".")[0];
// const delDate = new Date(item.deliveryDate).toISOString().split("T")[0];
// return {
// plantToken: plantToken[0].value,
// ...item,
// loadingDate: dateCon,
// deliveryDate: delDate,
// };
// });
return { success: true, message: "Current open orders", data: deliverys };
};

View File

@@ -13,7 +13,8 @@ import psiArticleData from "./route/getPsiArticleData.js";
import psiPlanningData from "./route/getPsiPlanningData.js";
import psiProductionData from "./route/getPsiProductionData.js";
import psiInventory from "./route/getPsiinventory.js";
import getForecastByAv from "./route/getForecastDataByAv.js";
import getDeliveryByDateRangeAndAv from "./route/getDeliveryDateByRangeAndAv.js";
const app = new OpenAPIHono();
const routes = [
@@ -23,6 +24,8 @@ const routes = [
getCustomerInv,
getOpenOrders,
getDeliveryByDate,
getDeliveryByDateRangeAndAv,
getForecastByAv,
fakeEDI,
addressCorrections,
fifoIndex,

View File

@@ -0,0 +1,58 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { responses } from "../../../globalUtils/routeDefs/responses.js";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { apiHit } from "../../../globalUtils/apiHits.js";
import { getDeliveryByDateRangeAndAv } from "../controller/getDeliveryByDateRangeAndAv.js";
const app = new OpenAPIHono({ strict: false });
const Body = z.object({
includeRunnningNumbers: z.string().openapi({ example: "x" }),
});
app.openapi(
createRoute({
tags: ["dataMart"],
summary: "Returns deliverys by daterange.",
method: "get",
path: "/deliverybydaterangeandav",
request: {
body: {
content: {
"application/json": { schema: Body },
},
},
},
responses: responses(),
}),
async (c) => {
const q: any = c.req.queries();
// make sure we have a vaid user being accessed thats really logged in
apiHit(c, { endpoint: "/deliverybydaterangeandav" });
const { data, error } = await tryCatch(
getDeliveryByDateRangeAndAv(
q["avs"] ? q["avs"][0] : null,
q["startDate"] ? q["startDate"][0] : null,
q["endDate"] ? q["endDate"][0] : null
)
);
if (error) {
console.log(error);
return c.json(
{
success: false,
message: "There was an error getting the deliveries.",
data: error,
},
400
);
}
return c.json({
success: data.success,
message: data.message,
data: data.data,
});
}
);
export default app;

View File

@@ -0,0 +1,61 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { responses } from "../../../globalUtils/routeDefs/responses.js";
import { tryCatch } from "../../../globalUtils/tryCatch.js";
import { apiHit } from "../../../globalUtils/apiHits.js";
import { psiGetPlanningData } from "../controller/psiGetPlanningData.js";
import { getForecastByAv } from "../controller/forecastByAvs.js";
const app = new OpenAPIHono({ strict: false });
const Body = z.object({
includeRunnningNumbers: z.string().openapi({ example: "x" }),
});
app.openapi(
createRoute({
tags: ["dataMart"],
summary: "Returns the psiarticleData.",
method: "get",
path: "/forecastbyav",
request: {
body: {
content: {
"application/json": { schema: Body },
},
},
},
responses: responses(),
}),
async (c) => {
const q: any = c.req.queries();
// make sure we have a vaid user being accessed thats really logged in
apiHit(c, { endpoint: "/forecastbyav" });
//console.log(articles["avs"][0]);
const { data, error } = await tryCatch(
getForecastByAv(q["avs"] ? q["avs"][0] : null)
);
if (error) {
console.log(error);
return c.json(
{
success: false,
message: "There was an error getting the forecast data.",
data: error,
},
400
);
}
//console.log(data);
return c.json(
{
success: data.success,
message: data.message,
data: data.data,
},
data.success ? 200 : 400
);
}
);
export default app;

View File

@@ -15,7 +15,7 @@ export const logCleanup = async () => {
.delete(logs)
.where(
and(
lte(logs.created_at, sql`NOW() - INTERVAL '3 days'`),
lte(logs.created_at, sql`NOW() - INTERVAL '300 days'`),
//inArray(logs.service, ["server", "tcp", "sqlProd", "globalutils","notify", "logger", "serverupdater"]),
eq(logs.level, "info")
)
@@ -25,14 +25,14 @@ export const logCleanup = async () => {
"info",
"lst",
"logger",
`${delLogs.length} Server logs were just deleted that were older than 3 days`
`${delLogs.length} Server logs were just deleted that were older than 300 days`
);
} catch (error) {
createLog(
"error",
"lst",
"logger",
`There was an error deleteing server logs. ${error}`
`There was an error deleting server logs. ${error}`
);
}
@@ -42,7 +42,7 @@ export const logCleanup = async () => {
.delete(logs)
.where(
and(
lte(logs.created_at, sql`NOW() - INTERVAL '7 days'`),
lte(logs.created_at, sql`NOW() - INTERVAL '700 days'`),
//inArray(logs.service, ["server", "tcp", "sqlProd", "globalutils", "notify", "logger", "serverupdater"]),
ne(logs.level, "info")
)
@@ -52,14 +52,14 @@ export const logCleanup = async () => {
"info",
"lst",
"logger",
`${delLogs.length} Server logs were just deleted that were older than 7 days`
`${delLogs.length} Server logs were just deleted that were older than 700 days`
);
} catch (error) {
createLog(
"error",
"lst",
"logger",
`There was an error deleteing server logs. ${error}`
`There was an error deleting server logs. ${error}`
);
}
};

View File

@@ -78,6 +78,7 @@ export const labelingProcess = async ({
// if we are running the zechettii
if (zechette) {
if (zechette.line === "0") return;
const macId = await getMac(zechette.line);
// filter out the lot for the line
filteredLot = lots.data.filter(
@@ -163,7 +164,11 @@ export const labelingProcess = async ({
}
// if there are more than 2 lots it might be an auto labeler, autolabeler will be defined by its id for now only dayton
if (filteredLot?.length > 2 && plantToken[0].value !== "usday1") {
const plantsCanHaveMultiLots = ["usday1", "usmcd1"];
if (
filteredLot?.length > 2 &&
!plantsCanHaveMultiLots.includes(plantToken[0].value)
) {
createLog(
"error",
"labeling",
@@ -205,59 +210,65 @@ export const labelingProcess = async ({
};
}
// check the material... mm,color (auto and manual combined), pkg
const mmStaged = await isMainMatStaged(filteredLot[0]);
// as we want to allow zechetti to print no matter what if we zechettii is true skip all this and just print the label
if (!zechette) {
// check the material... mm,color (auto and manual combined), pkg
const mmStaged = await isMainMatStaged(filteredLot[0]);
if (!mmStaged.success) {
createLog("error", "labeling", "ocp", mmStaged.message);
if (!mmStaged.success) {
createLog("error", "labeling", "ocp", mmStaged.message);
return {
success: false,
message: mmStaged.message,
};
}
return {
success: false,
message: mmStaged.message,
};
}
// comment only but will check for color
createLog(
"info",
"labeling",
"ocp",
`Remaining pallets for: ${filteredLot[0].MachineDescription} is ${filteredLot[0].Remaining}`
);
// do we want to over run
if (filteredLot[0].overPrinting === "no" && filteredLot[0].Remaining <= 0) {
// comment only but will check for color
createLog(
"error",
"info",
"labeling",
"ocp",
`Over Printing on ${filteredLot[0].MachineDescription} is not allowed please change the lot`
`Remaining pallets for: ${filteredLot[0].MachineDescription} is ${filteredLot[0].Remaining}`
);
// for slc we want to run the first label for henkel
firstLotLabel(filteredLot[0]);
return {
success: false,
message: `Over Printing on ${filteredLot[0].MachineDescription} is not allowed please change the lot`,
};
}
// prolink check by lot
const prolink = await prolinkCheck(filteredLot[0].lot);
// do we want to over run
if (
filteredLot[0].overPrinting === "no" &&
filteredLot[0].Remaining <= 0
) {
createLog(
"error",
"labeling",
"ocp",
`Over Printing on ${filteredLot[0].MachineDescription} is not allowed please change the lot`
);
// for slc we want to run the first label for henkel
firstLotLabel(filteredLot[0]);
return {
success: false,
message: `Over Printing on ${filteredLot[0].MachineDescription} is not allowed please change the lot`,
};
}
if (!prolink) {
//console.error(`Prolink does not match for ${filteredLot[0].MachineDescription}`);
createLog(
"error",
"labeling",
"ocp",
`Prolink does not match for ${filteredLot[0].MachineDescription}`
);
return {
success: false,
message: `Prolink does not match for ${filteredLot[0].MachineDescription}`,
};
// prolink check by lot
const prolink = await prolinkCheck(filteredLot[0].lot);
if (!prolink) {
//console.error(`Prolink does not match for ${filteredLot[0].MachineDescription}`);
createLog(
"error",
"labeling",
"ocp",
`Prolink does not match for ${filteredLot[0].MachineDescription}`
);
return {
success: false,
message: `Prolink does not match for ${filteredLot[0].MachineDescription}`,
};
}
createLog("info", "labeling", "ocp", `Is prolink good? ${prolink}`);
}
createLog("info", "labeling", "ocp", `Is prolink good? ${prolink}`);
// create the label
const label = await createLabel(filteredLot[0], userPrinted);
@@ -273,9 +284,12 @@ export const labelingProcess = async ({
}
// send over to be booked in if we can do it.
// same here if we are passing zechettii dont try to book in
const bookin = settingData.filter((s) => s.name === "bookin");
let book: any = [];
if (bookin[0].value === "1") {
if (bookin[0].value === "1" && !zechette) {
book = await bookInLabel(label.data);
if (!book.success) {

View File

@@ -42,7 +42,7 @@ export const manualPrint = async (manualPrint: any) => {
manualTag(
manualPrint.rfidTag,
"wrapper1",
parseInt(label.data.SSCC.slice(10, -1))
parseInt(label?.data.SSCC.slice(10, -1))
);
}
return {

View File

@@ -1,177 +0,0 @@
import { Controller, Tag } from "st-ethernet-ip";
import { labelingProcess } from "../../labeling/labelProcess.js";
import { createLog } from "../../../../logger/logger.js";
let plcAddress = "192.168.193.97"; // zechetti 2
let lastProcessedTimestamp = 0;
let PLC = new Controller() as any;
const labelerTag = new Tag("N7[0]"); // change the car to a or b depending on what zechetti.
//const t = new Tag("CONV_M01_SHTL_UNLD_IN_FROM_PREV_CONV_TRACK_CODE.PAL_ORIGIN_LINE_N") // this is for the new zechette to reach the pallet form
let pollingInterval: any = null;
let heartbeatInterval: any = null;
let reconnecting = false;
let lastTag = 0;
// Track last successful read
let lastHeartbeat: number = Date.now();
export async function zechitti1Connect() {
try {
createLog(
"info",
"zechitti1",
"ocp",
`Connecting to PLC at ${plcAddress}...`
);
await PLC.connect(plcAddress, 0);
createLog("info", "zechitti1", "ocp", "Zechetti 2 connected.");
// Start polling tags
startPolling();
// Start heartbeat
// startHeartbeat();
// Handle disconnects/errors
PLC.on("close", () => {
console.warn("PLC connection closed.");
handleReconnect();
});
PLC.on("error", (err: any) => {
createLog("error", "zechitti1", "ocp", `PLC error: ${err.message}`);
handleReconnect();
});
} catch (err: any) {
createLog(
"error",
"zechitti1",
"ocp",
`Initial connection failed: ${err.message}`
);
handleReconnect();
}
}
function startPolling() {
if (pollingInterval) clearInterval(pollingInterval);
pollingInterval = setInterval(async () => {
try {
await PLC.readTag(labelerTag);
//lastHeartbeat = Date.now();
const tagTime: any = new Date(labelerTag.timestamp);
// so we make sure we are not missing a pallet remove it from the lastTag so we can get this next label correctly
if (
labelerTag.value == 0 &&
Date.now() - lastProcessedTimestamp >= 45000
) {
lastTag = labelerTag.value;
}
// if the tag is not zero and its been longer than 30 seconds and the last tag is not equal to the current tag we can print
if (
labelerTag.value !== 0 &&
lastTag !== labelerTag.value &&
tagTime !== lastProcessedTimestamp &&
Date.now() - lastProcessedTimestamp >= 30000
) {
lastProcessedTimestamp = tagTime;
lastTag = labelerTag.value;
console.log(
`Time since last check: ${
Date.now() - tagTime
}, greater than 30000, ${
Date.now() - lastProcessedTimestamp >= 30000
}, the line to be printed is ${labelerTag.value}`
);
//console.log(labelerTag);
const zechette = {
line: labelerTag.value.toString(),
printer: 22, // this is the id of the zechetti 2 to print we should move this to the db
printerName: "Zechetti1",
};
labelingProcess({ zechette: zechette });
}
} catch (err: any) {
createLog(
"error",
"zechitti1",
"ocp",
`Polling error: ${err.message}`
);
handleReconnect();
}
}, 1000);
}
// function startHeartbeat() {
// if (heartbeatInterval) clearInterval(heartbeatInterval);
// heartbeatInterval = setInterval(() => {
// const diff = Date.now() - lastHeartbeat;
// if (diff > 60000) {
// // 1 minute
// console.warn(`⚠️ Heartbeat timeout: no data for ${diff / 1000}s`);
// handleReconnect();
// }
// }, 10000); // check every 10s
// }
async function handleReconnect() {
if (reconnecting) return;
reconnecting = true;
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
let delay = 2000; // start at 2s
let attempts = 0;
const maxAttempts = 10; // or limit by time, e.g. 2 min total
while (!PLC.connected && attempts < maxAttempts) {
attempts++;
createLog(
"info",
"zechitti1",
"ocp",
`Reconnect attempt ${attempts}/${maxAttempts} in ${
delay / 1000
}s...`
);
await new Promise((res) => setTimeout(res, delay));
try {
PLC = new Controller(); // fresh instance
await PLC.connect(plcAddress, 0);
createLog("info", "zechitti1", "ocp", "Reconnected to PLC!");
reconnecting = false;
startPolling();
return;
} catch (err: any) {
createLog(
"error",
"zechitti1",
"ocp",
`Reconnect attempt failed: ${err.message}`
);
delay = Math.min(delay * 2, 30000); // exponential backoff up to 30s
}
}
if (!PLC.connected) {
createLog(
"error",
"zechitti1",
"ocp",
"Max reconnect attempts reached. Stopping retries."
);
reconnecting = false;
// optional: exit process or alert someone here
// process.exit(1);
}
}

View File

@@ -0,0 +1,27 @@
import { createPlcMonitor } from "../../../utils/plcController.js";
export const zechettiConnect = () => {
const config: any = {
controllers: [
{
id: "Z1",
ip: "192.168.193.97",
slot: 0,
rpi: 250,
tags: ["N7[0]"],
},
{
id: "Z2",
ip: "192.168.193.111",
slot: 0,
rpi: 100,
tags: ["N8[0]"],
},
],
};
const monitor = createPlcMonitor(config);
// Start
monitor.start();
};

View File

@@ -17,7 +17,6 @@ import { assignedPrinters } from "./utils/checkAssignments.js";
import { printerCycle } from "./controller/printers/printerCycle.js";
import stopPrinterCycle from "./routes/printers/stopCycle.js";
import startPrinterCycle from "./routes/printers/startCycle.js";
import { printerCycleAutoLabelers } from "./controller/printers/printerCycleAutoLabelers.js";
import AutostartPrinterCycle from "./routes/printers/autoLabelerStart.js";
import AutostopPrinterCycle from "./routes/printers/autoLabelerStop.js";
import { deleteLabels } from "../../globalUtils/dbCleanUp/labelCleanUp.js";
@@ -26,7 +25,8 @@ import labelRatio from "./routes/labeling/getLabelRatio.js";
import resetRatio from "./routes/labeling/resetLabelRatio.js";
import materialTransferLot from "./routes/materials/lotTransfer.js";
import pendingTransfers from "./routes/materials/currentPending.js";
import { zechitti1Connect } from "./controller/specialProcesses/zechettis/zechetti1.js";
import { createPlcMonitor } from "./utils/plcController.js";
import { zechettiConnect } from "./controller/specialProcesses/zechettis/zechettiConnect.js";
const app = new OpenAPIHono();
@@ -85,7 +85,7 @@ setTimeout(() => {
// if zechetti plc is wanted we will connect
setTimeout(() => {
if (zechetti[0]?.value === "1") {
zechitti1Connect();
zechettiConnect();
}
}, 3 * 1000);

View File

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

View File

@@ -70,3 +70,75 @@ where CONVERT(date, Upd_Date) BETWEEN @StartDate AND @EndDate
order by Bol_PrintDate desc
`;
export const deliveryByDateRangeAndAv = `
use [test1_AlplaPROD2.0_Read]
SELECT
r.[ArticleHumanReadableId]
,[ReleaseNumber]
,h.CustomerOrderNumber
,x.CustomerLineItemNumber
,[CustomerReleaseNumber]
,[ReleaseState]
,[DeliveryState]
,ea.JournalNummer
,[ReleaseConfirmationState]
,[PlanningState]
,format(r.[OrderDate], 'yyyy-MM-dd HH:mm') as OrderDate
,FORMAT(r.[DeliveryDate], 'yyyy-MM-dd HH:mm') as DeliveryDate
,FORMAT(r.[LoadingDate], 'yyyy-MM-dd HH:mm') as LoadingDate
,[Quantity]
,[DeliveredQuantity]
,r.[AdditionalInformation1]
,r.[AdditionalInformation2]
,[TradeUnits]
,[LoadingUnits]
,[Trucks]
,[LoadingToleranceType]
,[SalesPrice]
,[Currency]
,[QuantityUnit]
,[SalesPriceRemark]
,r.[Remark]
,[Irradiated]
,r.[CreatedByEdi]
,[DeliveryAddressHumanReadableId]
,[CustomerArtNo]
,[TotalPrice]
,r.[ArticleAlias]
FROM [order].[Release] (nolock) as r
left join
[order].LineItem as x on
r.LineItemId = x.id
left join
[order].Header as h on
x.HeaderId = h.id
--bol stuff
left join
AlplaPROD_test1.dbo.V_LadePlanungenLadeAuftragAbruf (nolock) as zz
on zz.AbrufIdAuftragsAbruf = r.ReleaseNumber
left join
(select * from (SELECT
ROW_NUMBER() OVER (PARTITION BY IdJournal ORDER BY add_date DESC) AS RowNum
,*
FROM [AlplaPROD_test1].[dbo].[T_Lieferungen] (nolock)) x
where RowNum = 1) as ea on
zz.IdLieferschein = ea.IdJournal
where
r.ArticleHumanReadableId in ([articles])
--r.ReleaseNumber = 1452
and r.DeliveryDate between '[startDate]' and '[endDate]'
order by DeliveryDate desc
`;

View File

@@ -0,0 +1,17 @@
export const forecastByAvs = `
SELECT
[DeliveryAddressHumanReadableId]
,[DeliveryAddressDescription]
,[ArticleHumanReadableId]
,[ArticleDescription]
,[QuantityType]
,[CustomerArticleNumber]
,format([RequirementDate],'yyyy-MM-dd') RequirementDate
,[Quantity]
,[TradeUnits]
,[LoadingUnits]
,[ArticleAlias]
FROM [test1_AlplaPROD2.0_Read].[forecast].[Forecast] (nolock)
where ArticleHumanReadableId in ([articles])
`;

View File

@@ -0,0 +1,11 @@
export const getCurrentLabel = `
SELECT FORMAT(Add_Date,'yyyy-MM-dd HH:mm') as PrintTime
,[IdMaschine]
,[LfdNr]
FROM [AlplaPROD_test1].[dbo].[T_EtikettenGedruckt] (nolock)
where IdMaschine = [macId]
and FORMAT(Add_Date,'yyyy-MM-dd HH:mm') like '[time]'
order by Add_Date desc
`;

View File

@@ -0,0 +1,4 @@
ALTER TABLE "user_role" RENAME TO "user_roles";--> statement-breakpoint
ALTER TABLE "user_roles" DROP CONSTRAINT "user_role_user_id_user_id_fk";
--> statement-breakpoint
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,824 @@
{
"id": "2e845c85-5248-4082-a69a-77c18e50f044",
"prevId": "639e50c0-757a-4867-a78e-48e8e1a416fc",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.apikey": {
"name": "apikey",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"start": {
"name": "start",
"type": "text",
"primaryKey": false,
"notNull": false
},
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refill_interval": {
"name": "refill_interval",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"refill_amount": {
"name": "refill_amount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_refill_at": {
"name": "last_refill_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_enabled": {
"name": "rate_limit_enabled",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"rate_limit_time_window": {
"name": "rate_limit_time_window",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 86400000
},
"rate_limit_max": {
"name": "rate_limit_max",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 10
},
"request_count": {
"name": "request_count",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
},
"remaining": {
"name": "remaining",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"last_request": {
"name": "last_request",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"permissions": {
"name": "permissions",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"apikey_user_id_user_id_fk": {
"name": "apikey_user_id_user_id_fk",
"tableFrom": "apikey",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.jwks": {
"name": "jwks",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"last_login": {
"name": "last_login",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.logs": {
"name": "logs",
"schema": "",
"columns": {
"log_id": {
"name": "log_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"level": {
"name": "level",
"type": "text",
"primaryKey": false,
"notNull": false
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"subModule": {
"name": "subModule",
"type": "text",
"primaryKey": false,
"notNull": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stack": {
"name": "stack",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
},
"checked": {
"name": "checked",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.settings": {
"name": "settings",
"schema": "",
"columns": {
"settings_id": {
"name": "settings_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"moduleName": {
"name": "moduleName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": true
},
"roles": {
"name": "roles",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[\"systemAdmin\"]'::jsonb"
},
"add_User": {
"name": "add_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"add_Date": {
"name": "add_Date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"upd_User": {
"name": "upd_User",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'LST_System'"
},
"upd_date": {
"name": "upd_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"name": {
"name": "name",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user_roles": {
"name": "user_roles",
"schema": "",
"columns": {
"user_role_id": {
"name": "user_role_id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"module": {
"name": "module",
"type": "text",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"unique_user_module": {
"name": "unique_user_module",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "module",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_roles_user_id_user_id_fk": {
"name": "user_roles_user_id_user_id_fk",
"tableFrom": "user_roles",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -43,6 +43,13 @@
"when": 1758081790572,
"tag": "0005_gorgeous_purple_man",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1758588364052,
"tag": "0006_loud_reavers",
"breakpoints": true
}
]
}

View File

@@ -19,7 +19,7 @@
"database/testFiles/test-tiPostOrders.ts",
"scripts/translateScript.js",
"app/main.ts",
"app/src/types"
"types"
],
"exclude": [
"node_modules",

19
types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
// types/express/index.d.ts
import type { UserRoles } from "../app/src/pkg/db/schema/user_roles.ts";
declare global {
namespace Express {
interface User {
id: string;
email?: string;
username?: string;
roles: string[];
rawRoles: UserRoles[]; // keep raw drizzle rows if needed
}
interface Request {
user?: User;
}
}
}