Compare commits
7 Commits
8ead6ae166
...
cb2e6252e0
| Author | SHA1 | Date | |
|---|---|---|---|
| cb2e6252e0 | |||
| edbc7cefd8 | |||
| 8f1375ab7b | |||
| 4ab43d91b9 | |||
| bc6485ca9a | |||
| bc1cbbad2e | |||
| 4cc990f52f |
@@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: http://{{url}}/lst/api/me
|
url: http://{{url}}/lst/api/user/me
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
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 {
|
post {
|
||||||
url: http://{{url}}/lst/api/register
|
url: http://{{url}}/lst/api/user/register
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"username":"matthes01",
|
"username":"matthes011",
|
||||||
"name":"blake",
|
"name":"blake",
|
||||||
"email":"blake.matthes@alpla.com",
|
"email":"blake1.matthes@alpla.com",
|
||||||
"password":"nova0511"
|
"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());
|
app.use(express.json());
|
||||||
|
|
||||||
const allowedOrigins = [
|
const allowedOrigins = [
|
||||||
"http://localhost:5173", // dev
|
"http://localhost:5173", // lstV2 dev
|
||||||
"http://localhost:4200",
|
"http://localhost:5500", // lst dev
|
||||||
|
"http://localhost:4200", // express
|
||||||
env.BETTER_AUTH_URL, // prod
|
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 { auth } from "../../../pkg/auth/auth.js";
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
|
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// GET /health
|
// GET /health
|
||||||
router.get("/", async (req, res) => {
|
router.get("/", async (req: Request, res: Response) => {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: fromNodeHeaders(req.headers),
|
headers: fromNodeHeaders(req.headers),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { Router } from "express";
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { auth } from "../../../pkg/auth/auth.js";
|
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();
|
const router = Router();
|
||||||
|
|
||||||
@@ -18,6 +23,9 @@ const registerSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post("/", async (req: Request, res: Response) => {
|
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 {
|
try {
|
||||||
// Parse + validate incoming JSON against Zod schema
|
// Parse + validate incoming JSON against Zod schema
|
||||||
const validated = registerSchema.parse(req.body);
|
const validated = registerSchema.parse(req.body);
|
||||||
@@ -27,6 +35,9 @@ router.post("/", async (req: Request, res: Response) => {
|
|||||||
body: validated,
|
body: validated,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (totalUsers[0].count === 0) {
|
||||||
|
systemAdminRole(user.user.id);
|
||||||
|
}
|
||||||
return res.status(201).json(user);
|
return res.status(201).json(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof z.ZodError) {
|
if (err instanceof z.ZodError) {
|
||||||
@@ -36,8 +47,14 @@ router.post("/", async (req: Request, res: Response) => {
|
|||||||
details: flattened,
|
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 type { Express, Request, Response } from "express";
|
||||||
import me from "./me.js";
|
import me from "./me.js";
|
||||||
import register from "./register.js";
|
import register from "./register.js";
|
||||||
|
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||||
|
|
||||||
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
||||||
app.use(basePath + "/api/me", me);
|
app.use(basePath + "/api/user/me", requireAuth(), me);
|
||||||
app.use(basePath + "/api/auth/register", register);
|
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 healthRoutes from "./routes/healthRoutes.js";
|
||||||
import { setupAuthRoutes } from "../auth/routes/routes.js";
|
import { setupAuthRoutes } from "../auth/routes/routes.js";
|
||||||
|
import { setupAdminRoutes } from "../admin/routes.js";
|
||||||
|
|
||||||
export const setupRoutes = (app: Express, basePath: string) => {
|
export const setupRoutes = (app: Express, basePath: string) => {
|
||||||
// Root / health check
|
// Root / health check
|
||||||
@@ -9,6 +10,7 @@ export const setupRoutes = (app: Express, basePath: string) => {
|
|||||||
|
|
||||||
// all routes
|
// all routes
|
||||||
setupAuthRoutes(app, basePath);
|
setupAuthRoutes(app, basePath);
|
||||||
|
setupAdminRoutes(app, basePath);
|
||||||
|
|
||||||
// always try to go to the app weather we are in dev or in production.
|
// always try to go to the app weather we are in dev or in production.
|
||||||
app.get(basePath + "/", (req: Request, res: Response) => {
|
app.get(basePath + "/", (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -20,20 +20,26 @@ export const auth = betterAuth({
|
|||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema,
|
schema,
|
||||||
}),
|
}),
|
||||||
|
trustedOrigins: [
|
||||||
|
"*.alpla.net",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:5500",
|
||||||
|
],
|
||||||
appName: "lst",
|
appName: "lst",
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
minPasswordLength: 8, // optional config
|
minPasswordLength: 8, // optional config
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
jwt({ jwt: { expirationTime: "1h" } }),
|
//jwt({ jwt: { expirationTime: "1h" } }),
|
||||||
apiKey(),
|
apiKey(),
|
||||||
admin(),
|
admin(),
|
||||||
username(),
|
username(),
|
||||||
],
|
],
|
||||||
session: {
|
session: {
|
||||||
expiresIn: 60 * 60,
|
expiresIn: 60 * 60,
|
||||||
updateAge: 60 * 1,
|
updateAge: 60 * 5,
|
||||||
|
freshAge: 60 * 2,
|
||||||
cookieCache: {
|
cookieCache: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
maxAge: 5 * 60, // Cache duration in seconds
|
maxAge: 5 * 60, // Cache duration in seconds
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
|
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core";
|
||||||
import { user } from "./auth-schema.js";
|
import { user } from "./auth-schema.js";
|
||||||
|
|
||||||
export const userRole = pgTable(
|
export const userRoles = pgTable(
|
||||||
"user_role",
|
"user_roles",
|
||||||
{
|
{
|
||||||
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
|
userRoleId: uuid("user_role_id").defaultRandom().primaryKey(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
@@ -15,3 +15,4 @@ export const userRole = pgTable(
|
|||||||
uniqueIndex("unique_user_module").on(table.userId, table.module),
|
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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
1138
frontend/package-lock.json
generated
1138
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,19 +10,26 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@tanstack/react-form": "^1.23.0",
|
||||||
"@tanstack/react-query": "^5.89.0",
|
"@tanstack/react-query": "^5.89.0",
|
||||||
"@tanstack/react-router": "^1.131.36",
|
"@tanstack/react-router": "^1.131.36",
|
||||||
"@tanstack/react-router-devtools": "^1.131.36",
|
"@tanstack/react-router-devtools": "^1.131.36",
|
||||||
|
"axios": "^1.12.2",
|
||||||
"better-auth": "^1.3.11",
|
"better-auth": "^1.3.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13"
|
"tailwindcss": "^4.1.13",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
|
|||||||
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 { createAuthClient } from "better-auth/client";
|
||||||
import { usernameClient } from "better-auth/client/plugins";
|
import { usernameClient } from "better-auth/client/plugins";
|
||||||
export const authClient = createAuthClient({
|
import { create } from "zustand";
|
||||||
baseURL: `${window.location.origin}/lst/api/auth`, // 👈 This is fine
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
callbacks: {
|
import { useEffect } from "react";
|
||||||
onUpdate: (session: any) => console.log("Session updated", session),
|
import { useRouter } from "@tanstack/react-router";
|
||||||
onSignIn: (session: any) => console.log("Signed in!", session),
|
|
||||||
onSignOut: () => console.log("Signed out!"),
|
// ---- 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()],
|
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 "./index.css";
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
// Create a client
|
// 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({
|
||||||
const router = createRouter({ routeTree, basepath: "/lst/app" });
|
routeTree,
|
||||||
|
basepath: "/lst/app",
|
||||||
|
context: {
|
||||||
|
queryClient: {} as QueryClient,
|
||||||
|
//login: () => {},
|
||||||
|
//logout: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
declare module "@tanstack/react-router" {
|
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(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</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.
|
// 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 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 IndexRouteImport } from './routes/index'
|
||||||
import { Route as AdminSettingsRouteImport } from './routes/admin_/settings'
|
import { Route as authLoginRouteImport } from './routes/(auth)/login'
|
||||||
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'
|
|
||||||
|
|
||||||
const _adminRoute = _adminRouteImport.update({
|
|
||||||
id: '/__admin',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const AdminLayoutRouteRoute = AdminLayoutRouteRouteImport.update({
|
|
||||||
id: '/_adminLayout',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AdminSettingsRoute = AdminSettingsRouteImport.update({
|
const authLoginRoute = authLoginRouteImport.update({
|
||||||
id: '/admin_/settings',
|
id: '/(auth)/login',
|
||||||
path: '/admin/settings',
|
path: '/login',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/login': typeof authLoginRoute
|
||||||
'/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
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin/settings': typeof AdminSettingsRoute
|
'/login': typeof authLoginRoute
|
||||||
'/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
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/_adminLayout': typeof AdminLayoutRouteRouteWithChildren
|
'/(auth)/login': typeof authLoginRoute
|
||||||
'/__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
|
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths: '/' | '/login'
|
||||||
| '/'
|
|
||||||
| '/admin/settings'
|
|
||||||
| '/admin/servers'
|
|
||||||
| '/admin/users'
|
|
||||||
| '/admin'
|
|
||||||
| '/admin/servers/$serverId'
|
|
||||||
| '/admin/user/$userId'
|
|
||||||
| '/admin/servers/$serverId/edit'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to: '/' | '/login'
|
||||||
| '/'
|
id: '__root__' | '/' | '/(auth)/login'
|
||||||
| '/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'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AdminLayoutRouteRoute: typeof AdminLayoutRouteRouteWithChildren
|
authLoginRoute: typeof authLoginRoute
|
||||||
_adminRoute: typeof _adminRoute
|
|
||||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
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: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -170,97 +58,19 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/admin_/settings': {
|
'/(auth)/login': {
|
||||||
id: '/admin_/settings'
|
id: '/(auth)/login'
|
||||||
path: '/admin/settings'
|
path: '/login'
|
||||||
fullPath: '/admin/settings'
|
fullPath: '/login'
|
||||||
preLoaderRoute: typeof AdminSettingsRouteImport
|
preLoaderRoute: typeof authLoginRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
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 = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AdminLayoutRouteRoute: AdminLayoutRouteRouteWithChildren,
|
authLoginRoute: authLoginRoute,
|
||||||
_adminRoute: _adminRoute,
|
|
||||||
AdminSettingsRoute: AdminSettingsRoute,
|
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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 { 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 = () => (
|
interface RootRouteContext {
|
||||||
<>
|
queryClient: QueryClient;
|
||||||
<nav className="flex gap-1">
|
//user: User | null;
|
||||||
<Link to="/">Home</Link>
|
//login: (user: User) => void;
|
||||||
{/* <Link to="/admin">Admin</Link> */}
|
//logout: () => void;
|
||||||
</nav>
|
}
|
||||||
<hr></hr>
|
|
||||||
|
const RootLayout = () => {
|
||||||
|
//const { logout, login } = Route.useRouteContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SessionGuard>
|
||||||
|
<Nav />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
<Toaster expand={true} richColors closeButton />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</>
|
</SessionGuard>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createRootRoute({ component: RootLayout });
|
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,14 +1,18 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
|
import { useAuth } from "../lib/authClient";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: Index,
|
component: Index,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
|
const { session } = useAuth();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-screen flex items-center justify-center">
|
<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">
|
<Button className="h-96 w-96">
|
||||||
<a href="/lst/d" target="_blank" className="text-4xl">
|
<a href="/lst/d" target="_blank" className="text-4xl">
|
||||||
LST-DOCS
|
LST-DOCS
|
||||||
@@ -16,5 +20,6 @@ function Index() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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: {
|
server: {
|
||||||
|
port: 5500,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/lst/api": {
|
"/lst/api": {
|
||||||
target: `http://localhost:${Number(
|
target: `http://localhost:${Number(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const LoginForm = () => {
|
|||||||
const username = localStorage.getItem("username") || "";
|
const username = localStorage.getItem("username") || "";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = useSearch({ from: "/login" });
|
const search = useSearch({ from: "/login" });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function ManualPrintForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
//console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
handleManualPrintLog(data);
|
handleManualPrintLog(data);
|
||||||
};
|
};
|
||||||
@@ -243,8 +243,10 @@ export default function ManualPrintForm() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
//label="Comments"
|
//label="Comments"
|
||||||
placeholder="add more info as needed."
|
placeholder="add more info as needed."
|
||||||
{...(register("additionalComments"),
|
{
|
||||||
{ required: true, minLength: 10 })}
|
...register("additionalComments")
|
||||||
|
// { required: true, minLength: 10 }
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,17 +20,10 @@ export const SessionProvider = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModules();
|
fetchModules();
|
||||||
fetchSettings();
|
fetchSettings();
|
||||||
console.log("settings grab ran");
|
|
||||||
fetchUserRoles();
|
fetchUserRoles();
|
||||||
fetchSubModules();
|
fetchSubModules();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
//temp
|
|
||||||
localStorage.removeItem("ally-supports-cache");
|
|
||||||
localStorage.removeItem("auth-storage");
|
|
||||||
localStorage.removeItem("nextauth.message");
|
|
||||||
localStorage.removeItem("prod");
|
|
||||||
localStorage.removeItem("card-storage");
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const Route = createRootRoute({
|
|||||||
href={`https://${server[0].value}.alpla.net/lst/d`}
|
href={`https://${server[0].value}.alpla.net/lst/d`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
LST - Docs
|
LST - Docs |
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"dev:frontend": "cd frontend && npm run dev",
|
"dev:frontend": "cd frontend && npm run dev",
|
||||||
"dev:dbgen": " drizzle-kit generate --config=drizzle-dev.config.ts",
|
"dev:dbgen": " drizzle-kit generate --config=drizzle-dev.config.ts",
|
||||||
"dev:dbmigrate": " drizzle-kit migrate --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: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: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",
|
"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 jwt from "jsonwebtoken";
|
||||||
import {db} from "../../../../database/dbclient.js";
|
import { db } from "../../../../database/dbclient.js";
|
||||||
import {users} from "../../../../database/schema/users.js";
|
import { users } from "../../../../database/schema/users.js";
|
||||||
import {eq, sql} from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import {checkPassword} from "../utils/checkPassword.js";
|
import { checkPassword } from "../utils/checkPassword.js";
|
||||||
import {roleCheck} from "./userRoles/getUserAccess.js";
|
import { roleCheck } from "./userRoles/getUserAccess.js";
|
||||||
import {createLog} from "../../logger/logger.js";
|
import { createLog } from "../../logger/logger.js";
|
||||||
import {differenceInDays} from "date-fns";
|
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.
|
* Authenticate a user and return a JWT.
|
||||||
*/
|
*/
|
||||||
const {sign} = jwt;
|
const { sign } = jwt;
|
||||||
|
|
||||||
export async function login(
|
export async function login(
|
||||||
username: string,
|
username: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<{token: string; user: {user_id: string; username: string}}> {
|
): Promise<{ token: string; user: { user_id: string; username: string } }> {
|
||||||
const user = await db.select().from(users).where(eq(users.username, username));
|
const user = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.username, username));
|
||||||
|
|
||||||
//console.log(user);
|
//console.log(user);
|
||||||
if (user.length === 0) {
|
if (user.length === 0) {
|
||||||
@@ -48,23 +53,45 @@ export async function login(
|
|||||||
try {
|
try {
|
||||||
const lastLog = await db
|
const lastLog = await db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({lastLogin: sql`NOW()`})
|
.set({ lastLogin: sql`NOW()` })
|
||||||
.where(eq(users.user_id, user[0].user_id))
|
.where(eq(users.user_id, user[0].user_id))
|
||||||
.returning({lastLogin: users.lastLogin});
|
.returning({ lastLogin: users.lastLogin });
|
||||||
createLog(
|
createLog(
|
||||||
"info",
|
"info",
|
||||||
"lst",
|
"lst",
|
||||||
"auth",
|
"auth",
|
||||||
`Its been ${differenceInDays(lastLog[0]?.lastLogin ?? "", new Date(Date.now()))} days since ${
|
`Its been ${differenceInDays(
|
||||||
user[0].username
|
lastLog[0]?.lastLogin ?? "",
|
||||||
} has logged in`
|
new Date(Date.now())
|
||||||
|
)} days since ${user[0].username} has logged in`
|
||||||
);
|
);
|
||||||
//]);
|
//]);
|
||||||
} catch (error) {
|
} 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 psiPlanningData from "./route/getPsiPlanningData.js";
|
||||||
import psiProductionData from "./route/getPsiProductionData.js";
|
import psiProductionData from "./route/getPsiProductionData.js";
|
||||||
import psiInventory from "./route/getPsiinventory.js";
|
import psiInventory from "./route/getPsiinventory.js";
|
||||||
|
import getForecastByAv from "./route/getForecastDataByAv.js";
|
||||||
|
import getDeliveryByDateRangeAndAv from "./route/getDeliveryDateByRangeAndAv.js";
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -23,6 +24,8 @@ const routes = [
|
|||||||
getCustomerInv,
|
getCustomerInv,
|
||||||
getOpenOrders,
|
getOpenOrders,
|
||||||
getDeliveryByDate,
|
getDeliveryByDate,
|
||||||
|
getDeliveryByDateRangeAndAv,
|
||||||
|
getForecastByAv,
|
||||||
fakeEDI,
|
fakeEDI,
|
||||||
addressCorrections,
|
addressCorrections,
|
||||||
fifoIndex,
|
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)
|
.delete(logs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
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"]),
|
//inArray(logs.service, ["server", "tcp", "sqlProd", "globalutils","notify", "logger", "serverupdater"]),
|
||||||
eq(logs.level, "info")
|
eq(logs.level, "info")
|
||||||
)
|
)
|
||||||
@@ -25,14 +25,14 @@ export const logCleanup = async () => {
|
|||||||
"info",
|
"info",
|
||||||
"lst",
|
"lst",
|
||||||
"logger",
|
"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) {
|
} catch (error) {
|
||||||
createLog(
|
createLog(
|
||||||
"error",
|
"error",
|
||||||
"lst",
|
"lst",
|
||||||
"logger",
|
"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)
|
.delete(logs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
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"]),
|
//inArray(logs.service, ["server", "tcp", "sqlProd", "globalutils", "notify", "logger", "serverupdater"]),
|
||||||
ne(logs.level, "info")
|
ne(logs.level, "info")
|
||||||
)
|
)
|
||||||
@@ -52,14 +52,14 @@ export const logCleanup = async () => {
|
|||||||
"info",
|
"info",
|
||||||
"lst",
|
"lst",
|
||||||
"logger",
|
"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) {
|
} catch (error) {
|
||||||
createLog(
|
createLog(
|
||||||
"error",
|
"error",
|
||||||
"lst",
|
"lst",
|
||||||
"logger",
|
"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 we are running the zechettii
|
||||||
if (zechette) {
|
if (zechette) {
|
||||||
|
if (zechette.line === "0") return;
|
||||||
const macId = await getMac(zechette.line);
|
const macId = await getMac(zechette.line);
|
||||||
// filter out the lot for the line
|
// filter out the lot for the line
|
||||||
filteredLot = lots.data.filter(
|
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 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(
|
createLog(
|
||||||
"error",
|
"error",
|
||||||
"labeling",
|
"labeling",
|
||||||
@@ -205,6 +210,8 @@ export const labelingProcess = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// check the material... mm,color (auto and manual combined), pkg
|
||||||
const mmStaged = await isMainMatStaged(filteredLot[0]);
|
const mmStaged = await isMainMatStaged(filteredLot[0]);
|
||||||
|
|
||||||
@@ -226,7 +233,10 @@ export const labelingProcess = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// do we want to over run
|
// do we want to over run
|
||||||
if (filteredLot[0].overPrinting === "no" && filteredLot[0].Remaining <= 0) {
|
if (
|
||||||
|
filteredLot[0].overPrinting === "no" &&
|
||||||
|
filteredLot[0].Remaining <= 0
|
||||||
|
) {
|
||||||
createLog(
|
createLog(
|
||||||
"error",
|
"error",
|
||||||
"labeling",
|
"labeling",
|
||||||
@@ -258,6 +268,7 @@ export const labelingProcess = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
createLog("info", "labeling", "ocp", `Is prolink good? ${prolink}`);
|
createLog("info", "labeling", "ocp", `Is prolink good? ${prolink}`);
|
||||||
|
}
|
||||||
|
|
||||||
// create the label
|
// create the label
|
||||||
const label = await createLabel(filteredLot[0], userPrinted);
|
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.
|
// 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");
|
const bookin = settingData.filter((s) => s.name === "bookin");
|
||||||
let book: any = [];
|
let book: any = [];
|
||||||
if (bookin[0].value === "1") {
|
if (bookin[0].value === "1" && !zechette) {
|
||||||
book = await bookInLabel(label.data);
|
book = await bookInLabel(label.data);
|
||||||
|
|
||||||
if (!book.success) {
|
if (!book.success) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const manualPrint = async (manualPrint: any) => {
|
|||||||
manualTag(
|
manualTag(
|
||||||
manualPrint.rfidTag,
|
manualPrint.rfidTag,
|
||||||
"wrapper1",
|
"wrapper1",
|
||||||
parseInt(label.data.SSCC.slice(10, -1))
|
parseInt(label?.data.SSCC.slice(10, -1))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
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 { printerCycle } from "./controller/printers/printerCycle.js";
|
||||||
import stopPrinterCycle from "./routes/printers/stopCycle.js";
|
import stopPrinterCycle from "./routes/printers/stopCycle.js";
|
||||||
import startPrinterCycle from "./routes/printers/startCycle.js";
|
import startPrinterCycle from "./routes/printers/startCycle.js";
|
||||||
import { printerCycleAutoLabelers } from "./controller/printers/printerCycleAutoLabelers.js";
|
|
||||||
import AutostartPrinterCycle from "./routes/printers/autoLabelerStart.js";
|
import AutostartPrinterCycle from "./routes/printers/autoLabelerStart.js";
|
||||||
import AutostopPrinterCycle from "./routes/printers/autoLabelerStop.js";
|
import AutostopPrinterCycle from "./routes/printers/autoLabelerStop.js";
|
||||||
import { deleteLabels } from "../../globalUtils/dbCleanUp/labelCleanUp.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 resetRatio from "./routes/labeling/resetLabelRatio.js";
|
||||||
import materialTransferLot from "./routes/materials/lotTransfer.js";
|
import materialTransferLot from "./routes/materials/lotTransfer.js";
|
||||||
import pendingTransfers from "./routes/materials/currentPending.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();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ setTimeout(() => {
|
|||||||
// if zechetti plc is wanted we will connect
|
// if zechetti plc is wanted we will connect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (zechetti[0]?.value === "1") {
|
if (zechetti[0]?.value === "1") {
|
||||||
zechitti1Connect();
|
zechettiConnect();
|
||||||
}
|
}
|
||||||
}, 3 * 1000);
|
}, 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
|
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,
|
"when": 1758081790572,
|
||||||
"tag": "0005_gorgeous_purple_man",
|
"tag": "0005_gorgeous_purple_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758588364052,
|
||||||
|
"tag": "0006_loud_reavers",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"database/testFiles/test-tiPostOrders.ts",
|
"database/testFiles/test-tiPostOrders.ts",
|
||||||
"scripts/translateScript.js",
|
"scripts/translateScript.js",
|
||||||
"app/main.ts",
|
"app/main.ts",
|
||||||
"app/src/types"
|
"types"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"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