diff --git a/backend/socket.io/roomDefinitions.socket.ts b/backend/socket.io/roomDefinitions.socket.ts index b99c226..340ecca 100644 --- a/backend/socket.io/roomDefinitions.socket.ts +++ b/backend/socket.io/roomDefinitions.socket.ts @@ -1,3 +1,4 @@ +import { desc } from "drizzle-orm"; import { db } from "../db/db.controller.js"; import { logs } from "../db/schema/logs.schema.js"; import type { RoomId } from "./types.socket.js"; @@ -6,6 +7,11 @@ type RoomDefinition = { seed: (limit: number) => Promise; }; +export const protectedRooms: any = { + logs: { requiresAuth: true, role: "admin" }, + admin: { requiresAuth: true, role: "admin" }, +}; + export const roomDefinition: Record = { logs: { seed: async (limit) => { @@ -13,7 +19,7 @@ export const roomDefinition: Record = { const rows = await db .select() .from(logs) - .orderBy(logs.createdAt) + .orderBy(desc(logs.createdAt)) .limit(limit); return rows; //.reverse(); diff --git a/backend/socket.io/serverSetup.ts b/backend/socket.io/serverSetup.ts index 953a1de..893cd13 100644 --- a/backend/socket.io/serverSetup.ts +++ b/backend/socket.io/serverSetup.ts @@ -3,6 +3,7 @@ import type { Server as HttpServer } from "node:http"; //import { fileURLToPath } from "node:url"; import { instrument } from "@socket.io/admin-ui"; import { Server } from "socket.io"; +import { auth } from "utils/auth.utils.js"; import { createLogger } from "../logger/logger.controller.js"; import { allowedOrigins } from "../utils/cors.utils.js"; import { registerEmitter } from "./roomEmitter.socket.js"; @@ -12,6 +13,16 @@ import { createRoomEmitter, preseedRoom } from "./roomService.socket.js"; //const __dirname = dirname(__filename); const log = createLogger({ module: "socket.io", subModule: "setup" }); +//import type { Session, User } from "better-auth"; // adjust if needed +import { protectedRooms } from "./roomDefinitions.socket.js"; + +// declare module "socket.io" { +// interface Socket { +// user?: User | any; +// session?: Session; +// } +// } + export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => { const io = new Server(server, { path: `${baseUrl}/api/socket.io`, @@ -25,6 +36,38 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => { const { addDataToRoom } = createRoomEmitter(io); registerEmitter(addDataToRoom); + io.use(async (socket, next) => { + try { + //const cookieHeader = socket.handshake.headers.cookie; + const headers = new Headers(); + + for (const [key, value] of Object.entries(socket.request.headers)) { + if (typeof value === "string") { + headers.set(key, value); + } else if (Array.isArray(value)) { + headers.set(key, value.join(", ")); + } + } + + const session = await auth.api.getSession({ + headers, + }); + + if (!session) { + return next(); // allow connection, but no auth + } + + if (session) { + socket.user = session.user; + socket.session = session as any; + } + + next(); + } catch (err) { + next(); + } + }); + io.on("connection", (s) => { log.info({}, `User connected: ${s.id}`); @@ -35,6 +78,21 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => { }); s.on("join-room", async (rn) => { + const config = protectedRooms[rn]; + + if (config?.requiresAuth && !s.user) { + return s.emit("room-error", { + room: rn, + message: "Authentication required", + }); + } + + if (config?.role && s.user?.role !== config.role) { + return s.emit("room-error", { + room: rn, + message: `Not authorized to be in room: ${rn}`, + }); + } s.join(rn); // get room seeded diff --git a/backend/types/socket.d.ts b/backend/types/socket.d.ts new file mode 100644 index 0000000..75cf56d --- /dev/null +++ b/backend/types/socket.d.ts @@ -0,0 +1,9 @@ +import "socket.io"; +import type { Session, User } from "better-auth"; // adjust if needed + +declare module "socket.io" { + interface Socket { + user?: User | any; + session?: Session; + } +} diff --git a/backend/utils/auth.utils.ts b/backend/utils/auth.utils.ts index 82c4802..abcbeac 100644 --- a/backend/utils/auth.utils.ts +++ b/backend/utils/auth.utils.ts @@ -32,15 +32,15 @@ export const auth = betterAuth({ schema, }), trustedOrigins: allowedOrigins, - // user: { - // additionalFields: { - // role: { - // type: "string", - // //required: false, - // input: false, - // }, - // }, - // }, + user: { + additionalFields: { + role: { + type: "string", + //required: false, + input: false, + }, + }, + }, plugins: [ jwt({ jwt: { expirationTime: "1h" } }), //apiKey(), @@ -137,3 +137,5 @@ export const auth = betterAuth({ // }, }, }); + +type Session = typeof auth.$Infer.Session; diff --git a/brunoApi/environments/lstv3.bru b/brunoApi/environments/lstv3.bru index b2e9db9..ea6d279 100644 --- a/brunoApi/environments/lstv3.bru +++ b/brunoApi/environments/lstv3.bru @@ -1,3 +1,3 @@ vars { - url: http://uslim1vms006:3100/lst + url: http://localhost:3000/lst } diff --git a/brunoApi/system/updateSetting.bru b/brunoApi/system/updateSetting.bru index f5261da..1044810 100644 --- a/brunoApi/system/updateSetting.bru +++ b/brunoApi/system/updateSetting.bru @@ -12,8 +12,8 @@ patch { body:json { { - "value" : "0", - "active": "false" + "value" : "1", + "active": "true" } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c72188f..e1bf8c3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-form": "^1.28.5", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.166.7", "@tanstack/react-router-devtools": "^1.166.7", + "@tanstack/react-table": "^8.21.3", "axios": "^1.13.6", "better-auth": "^1.5.5", "class-variance-authority": "^0.7.1", @@ -3994,6 +3996,37 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz", + "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==", + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/form-core": { + "version": "1.28.5", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.5.tgz", + "integrity": "sha512-8lYnduHHfP6uaXF9+2OLnh3Fo27tH4TdtekWLG2b/Bp26ynbrWG6L4qhBgEb7VcvTpJw/RjvJF/JyFhZkG3pfQ==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.4.1", + "@tanstack/pacer-lite": "^0.1.1", + "@tanstack/store": "^0.9.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/history": { "version": "1.161.6", "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", @@ -4007,6 +4040,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/pacer-lite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz", + "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-core": { "version": "5.90.20", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", @@ -4017,6 +4063,28 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/react-form": { + "version": "1.28.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.28.5.tgz", + "integrity": "sha512-CL8IeWkeXnEEDsHt5wBuIOZvSYrKiLRtsC9ca0LzfJJ22SYCma9cBmh1UX1EBX0o3gH2U21PmUf+y5f9OJNoEQ==", + "license": "MIT", + "dependencies": { + "@tanstack/form-core": "1.28.5", + "@tanstack/react-store": "^0.9.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-start": { + "optional": true + } + } + }, "node_modules/@tanstack/react-query": { "version": "5.90.21", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", @@ -4103,6 +4171,26 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/router-core": { "version": "1.167.3", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.167.3.tgz", @@ -4284,6 +4372,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.161.6", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.161.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index a783ef0..2752dfe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,11 @@ "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-form": "^1.28.5", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.166.7", "@tanstack/react-router-devtools": "^1.166.7", + "@tanstack/react-table": "^8.21.3", "axios": "^1.13.6", "better-auth": "^1.5.5", "class-variance-authority": "^0.7.1", diff --git a/frontend/src/hooks/socket.io.hook.ts b/frontend/src/hooks/socket.io.hook.ts new file mode 100644 index 0000000..796f819 --- /dev/null +++ b/frontend/src/hooks/socket.io.hook.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import socket from "@/lib/socket.io"; + +export function useSocketRoom(roomId: string) { + const [data, setData] = useState([]); + const [info, setInfo] = useState( + "No data yet — join the room to start receiving", + ); + + useEffect(() => { + function handleConnect() { + socket.emit("join-room", roomId); + } + + function handleUpdate(payload: any) { + setData((prev) => [...payload.payloads, ...prev]); + } + + function handleError(err: any) { + setInfo(err.message ?? "Room error"); + } + + if (!socket.connected) { + socket.connect(); + } + + socket.on("connect", handleConnect); + socket.on("room-update", handleUpdate); + socket.on("room-error", handleError); + + // If already connected, join immediately + if (socket.connected) { + socket.emit("join-room", roomId); + } + + return () => { + socket.emit("leave-room", roomId); + + socket.off("connect", handleConnect); + socket.off("room-update", handleUpdate); + socket.off("room-error", handleError); + }; + }, [roomId]); + + return { data, info }; +} diff --git a/frontend/src/lib/socket.io.ts b/frontend/src/lib/socket.io.ts index f34ba99..6f62e97 100644 --- a/frontend/src/lib/socket.io.ts +++ b/frontend/src/lib/socket.io.ts @@ -3,6 +3,7 @@ import { io } from "socket.io-client"; // Connect to your Socket.io server const socket = io(`${window.location.host}`, { path: "/lst/api/socket.io", + withCredentials: true, // autoConnect: false, // connect manually // reconnection: true, // reconnectionAttempts: 5, diff --git a/frontend/src/routes/admin/logs.tsx b/frontend/src/routes/admin/logs.tsx index dc29e06..8f96019 100644 --- a/frontend/src/routes/admin/logs.tsx +++ b/frontend/src/routes/admin/logs.tsx @@ -1,7 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useSocketRoom } from "@/hooks/socket.io.hook"; import { authClient } from "@/lib/auth-client"; -import socket from "@/lib/socket.io"; export const Route = createFileRoute("/admin/logs")({ beforeLoad: async ({ location }) => { @@ -60,40 +59,49 @@ function LevelBadge({ level }: { level: number }) { } function RouteComponent() { + const { data: logs, info: logsInfo } = useSocketRoom("logs"); //const { user } = Route.useRouteContext(); //const router = useRouter(); - const [logs, setLogs] = useState([]); + // const [logs, setLogs] = useState([]); + // const [logsInfo, setLogInfo] = useState( + // "No logs yet — join the room to start receiving", + // ); - useEffect(() => { - // Connect if not already connected - if (!socket.connected) { - socket.connect(); - } + // useEffect(() => { + // // Connect if not already connected + // if (!socket.connected) { + // socket.connect(); + // } - socket.on("connect", () => { - socket.emit("join-room", "logs"); - }); + // socket.on("connect", () => { + // socket.emit("join-room", "logs"); + // }); - socket.emit("join-room", "logs"); - socket.on( - "room-update", - (data: { payloads: LogEntry[]; roomId: string }) => { - setLogs((prev) => [...data.payloads, ...prev]); - }, - ); + // socket.emit("join-room", "logs"); + // socket.on( + // "room-update", + // (data: { payloads: LogEntry[]; roomId: string }) => { + // setLogs((prev) => [...data.payloads, ...prev]); + // }, + // ); - // socket.on("logs", (data) => { - // console.log(data); - // setLogs((prev) => [...data.payloads, ...prev]); - // }); + // socket.on("room-error", (data) => { + // setLogInfo(data.message); + // }); - // Cleanup listeners on unmount - return () => { - socket.emit("leave-room", "logs"); - socket.off("room-update"); - socket.off("logs"); - }; - }, []); + // // socket.on("logs", (data) => { + // // console.log(data); + // // setLogs((prev) => [...data.payloads, ...prev]); + // // }); + + // // Cleanup listeners on unmount + // return () => { + // socket.emit("leave-room", "logs"); + // socket.off("room-update"); + // socket.off("room-error"); + // socket.off("logs"); + // }; + // }, []); return (
{/* Log Table */} @@ -115,7 +123,7 @@ function RouteComponent() { colSpan={6} className="text-center py-6 text-muted-foreground" > - No logs yet — join the room to start receiving + {logsInfo} ) : ( diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 0d79c36..7ed0642 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -27,5 +27,5 @@ "@/*": ["src/*"] } }, - "include": ["src"] + "include": ["src", "types"] } diff --git a/frontend/types/socket.d.ts b/frontend/types/socket.d.ts new file mode 100644 index 0000000..25e2179 --- /dev/null +++ b/frontend/types/socket.d.ts @@ -0,0 +1,9 @@ +import "socket.io"; +import type { Session, User } from "better-auth"; // adjust if needed + +declare module "socket.io" { + interface Socket { + user?: User; + session?: Session; + } +} diff --git a/frontend/src/vite-env.d.ts b/frontend/types/vite-env.d.ts similarity index 100% rename from frontend/src/vite-env.d.ts rename to frontend/types/vite-env.d.ts diff --git a/tsconfig.json b/tsconfig.json index 9a323d6..cfe0069 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ //"allowImportingTsExtensions": true, "noEmit": false }, - "include": ["backend/**/*"], + "include": ["backend/**/*", "types"], "exclude": [ "node_modules", "frontend",