Compare commits
7 Commits
8ead6ae166
...
cb2e6252e0
| Author | SHA1 | Date | |
|---|---|---|---|
| cb2e6252e0 | |||
| edbc7cefd8 | |||
| 8f1375ab7b | |||
| 4ab43d91b9 | |||
| bc6485ca9a | |||
| bc1cbbad2e | |||
| 4cc990f52f |
@@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://{{url}}/lst/api/me
|
||||
url: http://{{url}}/lst/api/user/me
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
24
LogisticsSupportTool_API_DOCS/app/auth/Get Users.bru
Normal file
24
LogisticsSupportTool_API_DOCS/app/auth/Get Users.bru
Normal 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
|
||||
}
|
||||
26
LogisticsSupportTool_API_DOCS/app/auth/GrantROle by ID.bru
Normal file
26
LogisticsSupportTool_API_DOCS/app/auth/GrantROle by ID.bru
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
35
LogisticsSupportTool_API_DOCS/app/auth/Session.bru
Normal file
35
LogisticsSupportTool_API_DOCS/app/auth/Session.bru
Normal 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
|
||||
}
|
||||
28
LogisticsSupportTool_API_DOCS/app/auth/Signout.bru
Normal file
28
LogisticsSupportTool_API_DOCS/app/auth/Signout.bru
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
|
||||
|
||||
75
app/src/internal/admin/controller/systemAdminRole.ts
Normal file
75
app/src/internal/admin/controller/systemAdminRole.ts
Normal 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");
|
||||
};
|
||||
19
app/src/internal/admin/routes.ts
Normal file
19
app/src/internal/admin/routes.ts
Normal 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
|
||||
);
|
||||
};
|
||||
52
app/src/internal/admin/routes/getUserRoles.ts
Normal file
52
app/src/internal/admin/routes/getUserRoles.ts
Normal 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;
|
||||
74
app/src/internal/admin/routes/grantRole.ts
Normal file
74
app/src/internal/admin/routes/grantRole.ts
Normal 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;
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
90
app/src/pkg/middleware/authMiddleware.ts
Normal file
90
app/src/pkg/middleware/authMiddleware.ts
Normal 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
12080
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
29
frontend/src/components/navBar/Nav.tsx
Normal file
29
frontend/src/components/navBar/Nav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
frontend/src/components/ui/card.tsx
Normal file
92
frontend/src/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
30
frontend/src/components/ui/checkbox.tsx
Normal file
30
frontend/src/components/ui/checkbox.tsx
Normal 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 }
|
||||
21
frontend/src/components/ui/input.tsx
Normal file
21
frontend/src/components/ui/input.tsx
Normal 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 }
|
||||
22
frontend/src/components/ui/label.tsx
Normal file
22
frontend/src/components/ui/label.tsx
Normal 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 }
|
||||
23
frontend/src/components/ui/lstCard.tsx
Normal file
23
frontend/src/components/ui/lstCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
183
frontend/src/components/ui/select.tsx
Normal file
183
frontend/src/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
34
frontend/src/lib/formStuff/components/CheckBox.tsx
Normal file
34
frontend/src/lib/formStuff/components/CheckBox.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
16
frontend/src/lib/formStuff/components/FieldErrors.tsx
Normal file
16
frontend/src/lib/formStuff/components/FieldErrors.tsx
Normal 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>
|
||||
));
|
||||
};
|
||||
28
frontend/src/lib/formStuff/components/InputField.tsx
Normal file
28
frontend/src/lib/formStuff/components/InputField.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
57
frontend/src/lib/formStuff/components/SelectField.tsx
Normal file
57
frontend/src/lib/formStuff/components/SelectField.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
24
frontend/src/lib/formStuff/components/SubmitButton.tsx
Normal file
24
frontend/src/lib/formStuff/components/SubmitButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
frontend/src/lib/formStuff/index.tsx
Normal file
15
frontend/src/lib/formStuff/index.tsx
Normal 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,
|
||||
});
|
||||
18
frontend/src/lib/providers/SessionProvider.tsx
Normal file
18
frontend/src/lib/providers/SessionProvider.tsx
Normal 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}</>;
|
||||
}
|
||||
17
frontend/src/lib/querys/session.ts
Normal file
17
frontend/src/lib/querys/session.ts
Normal 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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
75
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal file
75
frontend/src/routes/(auth)/-components/LoginForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
frontend/src/routes/(auth)/login.tsx
Normal file
30
frontend/src/routes/(auth)/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
6
frontend/src/types/index.ts
Normal file
6
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type User = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
@@ -35,6 +35,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5500,
|
||||
proxy: {
|
||||
"/lst/api": {
|
||||
target: `http://localhost:${Number(
|
||||
|
||||
@@ -23,6 +23,7 @@ const LoginForm = () => {
|
||||
const username = localStorage.getItem("username") || "";
|
||||
const router = useRouter();
|
||||
const search = useSearch({ from: "/login" });
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
// }
|
||||
|
||||
// };
|
||||
|
||||
47
lstV2/server/services/dataMart/controller/forecastByAvs.ts
Normal file
47
lstV2/server/services/dataMart/controller/forecastByAvs.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
61
lstV2/server/services/dataMart/route/getForecastDataByAv.ts
Normal file
61
lstV2/server/services/dataMart/route/getForecastDataByAv.ts
Normal 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;
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
155
lstV2/server/services/ocp/utils/plcController.ts
Normal file
155
lstV2/server/services/ocp/utils/plcController.ts
Normal 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 };
|
||||
};
|
||||
@@ -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
|
||||
|
||||
`;
|
||||
|
||||
17
lstV2/server/services/sqlServer/querys/dataMart/forecast.ts
Normal file
17
lstV2/server/services/sqlServer/querys/dataMart/forecast.ts
Normal 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])
|
||||
`;
|
||||
11
lstV2/server/services/sqlServer/querys/ocp/getLabel.ts
Normal file
11
lstV2/server/services/sqlServer/querys/ocp/getLabel.ts
Normal 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
|
||||
`;
|
||||
4
migrations/0006_loud_reavers.sql
Normal file
4
migrations/0006_loud_reavers.sql
Normal 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;
|
||||
824
migrations/meta/0006_snapshot.json
Normal file
824
migrations/meta/0006_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
19
types/express.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user