Merged in release/2024-09-27 (pull request #1793)

Release/2024 09 27 IO-2924, IO-2931, IO-2935, IO-2938
This commit is contained in:
Patrick Fic
2024-09-26 23:07:20 +00:00
46 changed files with 3138 additions and 2007 deletions

20
certs/cert.pem Normal file
View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIUD/QBSAXy/AlJ/cS4DaPWJLpChxgwDQYJKoZIhvcNAQEL
BQAwPTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQwOTA5MTU0MjA1WhcNMjUwOTA5MTU0MjA1
WjA9MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AKSd0l7NJCNBwvtPU+dVPQkteg0AfC3sGqRnZMQteCRVa2oIgC4NoF3A9BK/yHbF
ZF25OnXTck5vzc8yb3v73ndfTD9ASKNoiaZE84/GFBsxqlKR8cs0qVwzuAsdijMv
vlMPNlMRyE1Rb7nR6HXGkPXNyxgMko03NXPkvIje9zRudm0Lf8L4q/hPyPkS7Mrm
/uQfAAJe+xFcupkEX2XY7r0x1C+z6E8lA1UcuhK3SHdW7CWYqp1vU5/dnnUiXwCa
GiC6Y1bCJB0pDAVISzy3JUDdINZdiqGR+y8ho3pstChf2mp/76s3N9eG9KA/qaFK
BrGk2PvCoZ8/Aj1aMsRYFHECAwEAAaNTMFEwHQYDVR0OBBYEFDLJ2fbWP4VUJgOp
PSs+NGHcVgRmMB8GA1UdIwQYMBaAFDLJ2fbWP4VUJgOpPSs+NGHcVgRmMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABfv5ut/y03atq0NMB0jeDY4
AvW4ukk0k1svyqxFZCw9o7m2lHb/IjmVrZG1Sj4JWrrSv0s02ccb26/t6vazNa5L
Powe3eyfHgfjTZJmgs8hyeMwKS0wWk/SPuu9JDhIJakiquqD+UVBGkHpP+XYvhDv
vhS2XRlW+aEjpUmr1oCyyrc6WbzrYRNadqEsn/AxwcMyUbht3Ugjkg+OpidcTIQp
5lv63waKo6I1vQofzBQ3L7JYsKo8kC0vAP7wkLxvzBii335uZJzzpFYFVOyVNezi
dJdazPbRYbXz4LjltdEn/SNfRuKX8ZRiN2OSo7OfSrZaMTS87SfCSFJGgQM8Yrk=
-----END CERTIFICATE-----

28
certs/key.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkndJezSQjQcL7
T1PnVT0JLXoNAHwt7BqkZ2TELXgkVWtqCIAuDaBdwPQSv8h2xWRduTp103JOb83P
Mm97+953X0w/QEijaImmRPOPxhQbMapSkfHLNKlcM7gLHYozL75TDzZTEchNUW+5
0eh1xpD1zcsYDJKNNzVz5LyI3vc0bnZtC3/C+Kv4T8j5EuzK5v7kHwACXvsRXLqZ
BF9l2O69MdQvs+hPJQNVHLoSt0h3VuwlmKqdb1Of3Z51Il8AmhogumNWwiQdKQwF
SEs8tyVA3SDWXYqhkfsvIaN6bLQoX9pqf++rNzfXhvSgP6mhSgaxpNj7wqGfPwI9
WjLEWBRxAgMBAAECggEAUNpHYlLFxh9dokujPUMreF+Cy/IKDBAkQc2au5RNpyLh
YDIOqw/8TTAhcTgLQPLQygvZP9f8E7RsVLFD+pSJ/v2qmIJ9au1Edor1Sg+S/oxV
SLrwFMunx2aLpcH7iAqSI3+cQg7A3+D4zD7iOz6tIl3Su9wo+v073tFhHKTOrEwv
Qgr9Jf3viIiKV1ym+uQEVQndocfsj46FnFpXTQ2qs7kAF6FgAOLDGfQQwzkiqEBD
NsqsDmbYIx6foZL+DEz1ZVO2M5B+xxpbNK82KwuQilVpimW8ui4LZHCe+RIFzt9+
BK6KGlLpSEwTFliivI3nahy18JzskZsfyah0CPZlQQKBgQDVv+A0qIPGvOP3Sx+9
HyeQCV23SkvvSvw8p8pMB0gvwv63YdJ7N/rJzBGS6YUHFWWZZgEeTgkJ6VJvoe0r
8JL1el9uSUa7f0eayjmFBOGuzpktNVdIn2Tg7A9MWA4JqPNNC69RMOh86ewGD4J3
a8Hz2a1bHxAmy/AZt2ukypY6eQKBgQDFJ7kqeOPkRBz9WbALRgVIXo8YWf5di0sQ
r0HC03GAISHQ725A2IFBPHJWeqj0jaMiIZD0y+Obgp7KAskrJaLfsd7Ug775kFfw
oUI9UAl6kRuPKvm3BaVAm46SQm+56VsgxTi73YN0NUp75THHZgAJjepF9zSpVJxq
VY9DjEGruQKBgQCQCpGIatcCol/tUg69X7VFd0pULhkl1J5OMbQ9r9qRdRI5eg5h
QsQaIQ7mtb8TmvOwf/DY/zVQHI+U8sXlCmW+TwzoQTENQSR7xzMj1LpRFqBaustr
AR72A537kItFLzll/i3SxOam5uxK2UDOQSuerF4KPdCglGXkrpo3nt3F4QKBgQCa
RArPAOjQo7tLQfJN3+wiRFsTYtd1uphx5bA/EdOtvj8HjVFnzADXWsTchf3N3UXY
XwtdgGwIMpys1KEz8a8P+c2x26SDAj7NOmDqOMYx8Xju/WGHpBM6Cn30U6e4gK+d
ZLSPyzQgqdIuP5hDvbwpvbGiLVw3Ys1BJtGCuSxpgQJ/eHnRiuSi5Zq5jGg+GpA+
FEEc9NCy772rR+4uzEOqyIwqewffqzSuVWuIsY/8MP3fh+NDxl/mU6cB5QVeD54Z
JZUKwmpM26muiM6WvVnM4ExPdSGA2+l4pZjby/KKd6F/w0tgZ1jb9Pb2/0vN3qVA
Y4U4XNTMt2fxUACqiL4SHA==
-----END PRIVATE KEY-----

View File

@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000 VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX

View File

@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000 VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=PROMANAGER VITE_APP_INSTANCE=PROMANAGER

View File

@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo' VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000 VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA VITE_APP_COUNTRY=USA

1
client/.gitignore vendored
View File

@@ -1,3 +1,4 @@
# Sentry Config File # Sentry Config File
.sentryclirc .sentryclirc
/dev-dist

View File

@@ -12,6 +12,6 @@ module.exports = defineConfig({
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
return require("./cypress/plugins/index.js")(on, config); return require("./cypress/plugins/index.js")(on, config);
}, },
baseUrl: "http://localhost:3000" baseUrl: "https://localhost:3000"
} }
}); });

View File

@@ -17,7 +17,7 @@
<meta name="theme-color" content="#1690ff"/> <meta name="theme-color" content="#1690ff"/>
<!-- <link rel="apple-touch-icon" href="logo192.png" /> --> <!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.--> <!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="public/logo192.png"/> <link rel="apple-touch-icon" href="/logo192.png"/>
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF"> <link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a

2123
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,37 +9,37 @@
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@ant-design/pro-layout": "^7.19.12", "@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.4", "@apollo/client": "^3.11.8",
"@emotion/is-prop-valid": "^1.3.0", "@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.4.3", "@fingerprintjs/fingerprintjs": "^4.5.0",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.33.1", "@sentry/cli": "^2.36.2",
"@sentry/react": "^7.114.0", "@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.12.1", "@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51", "@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1", "antd": "^5.20.1",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0", "apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.7.4", "axios": "^1.7.7",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"css-box-model": "^1.2.1", "css-box-model": "^1.2.1",
"dayjs": "^1.11.12", "dayjs": "^1.11.13",
"dayjs-business-days2": "^1.2.2", "dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.12.5", "firebase": "^10.13.2",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"i18next": "^23.12.3", "i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.5", "libphonenumber-js": "^1.11.9",
"logrocket": "^8.1.2", "logrocket": "^8.1.2",
"markerjs2": "^2.32.1", "markerjs2": "^2.32.2",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.0.1", "normalize-url": "^8.0.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -47,7 +47,7 @@
"query-string": "^9.1.0", "query-string": "^9.1.0",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-big-calendar": "^1.13.2", "react-big-calendar": "^1.14.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -58,15 +58,15 @@
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-number-format": "^5.4.0", "react-number-format": "^5.4.2",
"react-popopo": "^2.1.9", "react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.6", "react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.2",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.1", "react-virtuoso": "^4.10.4",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-actions": "^3.0.3", "redux-actions": "^3.0.3",
@@ -74,12 +74,12 @@
"redux-saga": "^1.3.0", "redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^5.1.1", "reselect": "^5.1.1",
"sass": "^1.77.8", "sass": "^1.79.3",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.8.0",
"styled-components": "^6.1.12", "styled-components": "^6.1.13",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3", "use-memo-one": "^1.1.3",
"userpilot": "^1.3.5", "userpilot": "^1.3.6",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2" "web-vitals": "^3.5.2"
}, },
@@ -91,6 +91,9 @@
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite", "start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite", "start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite", "start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
"build:test:imex": "env-cmd -f .env.test.imex npm run build", "build:test:imex": "env-cmd -f .env.test.imex npm run build",
"build:test:rome": "env-cmd -f .env.test.rome npm run build", "build:test:rome": "env-cmd -f .env.test.rome npm run build",
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build", "build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
@@ -131,29 +134,29 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.7.0", "@dotenvx/dotenvx": "^1.14.1",
"@emotion/babel-plugin": "^11.12.0", "@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.3",
"@sentry/webpack-plugin": "^2.22.2", "@sentry/webpack-plugin": "^2.22.4",
"@testing-library/cypress": "^10.0.2", "@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3", "browserslist": "^4.23.3",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.13.3", "cypress": "^13.14.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.11.1", "memfs": "^4.12.0",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11", "react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
"vite": "^5.4.0", "vite": "^5.4.7",
"vite-plugin-babel": "^1.2.0", "vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1", "vite-plugin-pwa": "^0.20.5",
"vite-plugin-style-import": "^2.0.0", "vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0" "workbox-window": "^7.1.0"
} }

View File

@@ -21,6 +21,7 @@ import "./App.styles.scss";
import Eula from "../components/eula/eula.component"; import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr"; import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx"; import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -201,7 +202,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/manage/*" path="/manage/*"
element={ element={
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} /> <SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary> </ErrorBoundary>
} }
> >
@@ -211,7 +214,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/tech/*" path="/tech/*"
element={ element={
<ErrorBoundary> <ErrorBoundary>
<PrivateRoute isAuthorized={currentUser.authorized} /> <SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary> </ErrorBoundary>
} }
> >

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useContext, useEffect, useMemo, useRef } from "react";
import { useQuery, useSubscription } from "@apollo/client"; import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import {
QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_JOBS_IN_PRODUCTION, QUERY_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION,
SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW
@@ -10,6 +11,8 @@ import {
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries"; import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component"; import ProductionBoardKanbanComponent from "./production-board-kanban.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -17,7 +20,17 @@ const mapStateToProps = createStructuredSelector({
}); });
function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) { function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) {
const fired = useRef(false); // useRef to keep track of whether the subscription fired const fired = useRef(false);
const client = useApolloClient();
const { socket } = useContext(SocketContext); // Get the socket from context
const {
treatments: { Websocket_Production }
} = useSplitTreatments({
attributes: {},
names: ["Websocket_Production"],
splitKey: bodyshop && bodyshop.imexshopid
});
const combinedStatuses = useMemo( const combinedStatuses = useMemo(
() => [ () => [
@@ -34,9 +47,12 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`) onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
}); });
const subscriptionEnabled = Websocket_Production?.treatment === "off";
const { data: updatedJobs } = useSubscription( const { data: updatedJobs } = useSubscription(
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION, subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
{ {
skip: !subscriptionEnabled,
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
} }
); );
@@ -46,22 +62,87 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`) onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
}); });
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
useEffect(() => { useEffect(() => {
if (!updatedJobs) { if (subscriptionEnabled) {
if (!updatedJobs) {
return;
}
if (!fired.current) {
fired.current = true;
return;
}
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, refetch, subscriptionEnabled]);
// Socket.IO implementation for users with Split treatment "off"
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
return; return;
} }
if (!fired.current) {
fired.current = true; const handleJobUpdates = async (jobChangedData) => {
return; const jobId = jobChangedData.id;
}
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); // Access the existing cache for QUERY_JOBS_IN_PRODUCTION
}, [updatedJobs, refetch]); const existingJobsCache = client.readQuery({
query: QUERY_JOBS_IN_PRODUCTION
});
const existingJobs = existingJobsCache?.jobs || [];
// Check if the job already exists in the cached jobs
const existingJob = existingJobs.find((job) => job.id === jobId);
if (existingJob) {
// If the job exists, we update the cache without making any additional queries
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: existingJobs.map((job) =>
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
)
}
});
} else {
// If the job doesn't exist, fetch it from the server and then add it to the cache
try {
const { data: jobData } = await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
});
// Add the job to the existing cached jobs
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
}
});
} catch (error) {
console.error(`Error fetching job ${jobId}: ${error.message}`);
}
}
};
const handleReconnect = () => {
//If we were disconnected from the board, we missed stuff. We need to refresh it entirely.
if (refetch) refetch();
};
// Listen for 'job-changed' events
socket.on("production-job-updated", handleJobUpdates);
socket.on("reconnect", handleReconnect);
// Clean up on unmount or when dependencies change
return () => {
socket.off("production-job-updated", handleJobUpdates);
socket.off("reconnect", handleReconnect);
};
}, [subscriptionEnabled, socket, bodyshop, data, client, refetch]);
const filteredAssociationSettings = useMemo(() => { const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null; return associationSettings?.associations[0] || null;
}, [associationSettings]); }, [associationSettings?.associations]);
return ( return (
<ProductionBoardKanbanComponent <ProductionBoardKanbanComponent

View File

@@ -17,7 +17,6 @@
border-radius: 5px 5px 0 0; border-radius: 5px 5px 0 0;
} }
.production-alert { .production-alert {
background: transparent; background: transparent;
border: none; border: none;
@@ -70,3 +69,8 @@
} }
} }
} }
.clone.is-dragging .ant-card {
border: #1890ff 2px solid !important;
border-radius: 12px;
}

View File

@@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) {
const axis = candidate.axis; const axis = candidate.axis;
const target = patch( const target = patch(
candidate.axis.line, candidate.axis.line,
// use the current center of the dragging item on the main axis // use the center of the list on the main axis
pageBorderBox.center[axis.line], candidate.page.borderBox.center[axis.line],
// use the center of the list on the cross axis // use the center of the list on the cross axis
candidate.page.borderBox.center[axis.crossAxisLine] candidate.page.borderBox.center[axis.crossAxisLine]
); );

View File

@@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element";
const isEqual = (base) => (value) => base === value; const isEqual = (base) => (value) => base === value;
const isScroll = isEqual("scroll"); const isScroll = isEqual("scroll");
const isAuto = isEqual("auto"); const isAuto = isEqual("auto");
const isOverlay = isEqual("overlay");
const isVisible = isEqual("visible"); const isVisible = isEqual("visible");
const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY); const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY);
const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY); const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY);
@@ -14,7 +15,7 @@ const isElementScrollable = (el) => {
overflowX: style.overflowX, overflowX: style.overflowX,
overflowY: style.overflowY overflowY: style.overflowY
}; };
return isEither(overflow, isScroll) || isEither(overflow, isAuto); return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay);
}; };
// Special case for a body element // Special case for a body element

View File

@@ -8,7 +8,7 @@ function getSelector(contextId) {
return `[${attributes.dragHandle.contextId}="${contextId}"]`; return `[${attributes.dragHandle.contextId}="${contextId}"]`;
} }
function findClosestDragHandleFromEvent(contextId, event) { export function findClosestDragHandleFromEvent(contextId, event) {
const target = event.target; const target = event.target;
if (!isElement(target)) { if (!isElement(target)) {
warning("event.target must be a Element"); warning("event.target must be a Element");

View File

@@ -240,11 +240,14 @@ export default function useTouchSensor(api) {
y: clientY y: clientY
}; };
const handle = api.findClosestDragHandle(event);
invariant(handle, "Touch sensor unable to find drag handle");
// unbind this event handler // unbind this event handler
unbindEventsRef.current(); unbindEventsRef.current();
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
startPendingDrag(actions, point); startPendingDrag(actions, point, handle);
} }
}), }),
// not including stop or startPendingDrag as it is not defined initially // not including stop or startPendingDrag as it is not defined initially
@@ -288,7 +291,7 @@ export default function useTouchSensor(api) {
} }
}, [stop]); }, [stop]);
const bindCapturingEvents = useCallback( const bindCapturingEvents = useCallback(
function bindCapturingEvents() { function bindCapturingEvents(target) {
const options = { const options = {
capture: true, capture: true,
passive: false passive: false
@@ -307,7 +310,7 @@ export default function useTouchSensor(api) {
// Old behaviour: // Old behaviour:
// https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d // https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d
// https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed
const unbindTarget = bindEvents(window, getHandleBindings(args), options); const unbindTarget = bindEvents(target, getHandleBindings(args), options);
const unbindWindow = bindEvents(window, getWindowBindings(args), options); const unbindWindow = bindEvents(window, getWindowBindings(args), options);
unbindEventsRef.current = function unbindAll() { unbindEventsRef.current = function unbindAll() {
unbindTarget(); unbindTarget();
@@ -330,7 +333,7 @@ export default function useTouchSensor(api) {
[getPhase, setPhase] [getPhase, setPhase]
); );
const startPendingDrag = useCallback( const startPendingDrag = useCallback(
function startPendingDrag(actions, point) { function startPendingDrag(actions, point, target) {
invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag"); invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag");
const longPressTimerId = setTimeout(startDragging, timeForLongPress); const longPressTimerId = setTimeout(startDragging, timeForLongPress);
setPhase({ setPhase({
@@ -339,7 +342,7 @@ export default function useTouchSensor(api) {
actions, actions,
longPressTimerId longPressTimerId
}); });
bindCapturingEvents(); bindCapturingEvents(target);
}, },
[bindCapturingEvents, getPhase, setPhase, startDragging] [bindCapturingEvents, getPhase, setPhase, startDragging]
); );

View File

@@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position";
import { warning } from "../../dev-warning"; import { warning } from "../../dev-warning";
import useLayoutEffect from "../use-isomorphic-layout-effect"; import useLayoutEffect from "../use-isomorphic-layout-effect";
import { noop } from "../../empty"; import { noop } from "../../empty";
import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event"; import findClosestDraggableIdFromEvent, {
findClosestDragHandleFromEvent
} from "./find-closest-draggable-id-from-event";
import findDraggable from "../get-elements/find-draggable"; import findDraggable from "../get-elements/find-draggable";
import bindEvents from "../event-bindings/bind-events"; import bindEvents from "../event-bindings/bind-events";
@@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
}), }),
[contextId, lockAPI, registry, store] [contextId, lockAPI, registry, store]
); );
const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]);
const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]); const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
const findOptionsForDraggable = useCallback( const findOptionsForDraggable = useCallback(
(id) => { (id) => {
@@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen
findClosestDraggableId, findClosestDraggableId,
findOptionsForDraggable, findOptionsForDraggable,
tryReleaseLock, tryReleaseLock,
isLockClaimed isLockClaimed,
findClosestDragHandle
}), }),
[canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed] [
canGetLock,
tryGetLock,
findClosestDraggableId,
findOptionsForDraggable,
tryReleaseLock,
isLockClaimed,
findClosestDragHandle
]
); );
// Bad ass // Bad ass

View File

@@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => {
return { return {
selector: getSelector(attributes.draggable.contextId), selector: getSelector(attributes.draggable.contextId),
styles: { styles: {
dragging: transition, dragging: `
${transition}
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
`,
dropAnimating: transition, dropAnimating: transition,
userCancel: transition userCancel: transition
} }

View File

@@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) {
const remove = (ref) => { const remove = (ref) => {
const current = ref.current; const current = ref.current;
invariant(current, "Cannot unmount ref as it is not set"); invariant(current, "Cannot unmount ref as it is not set");
getHead().removeChild(current); if (getHead().contains(current)) {
getHead().removeChild(current);
}
ref.current = null; ref.current = null;
}; };
remove(alwaysRef); remove(alwaysRef);

View File

@@ -1,5 +1,5 @@
import { useApolloClient, useQuery, useSubscription } from "@apollo/client"; import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
import React, { useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { import {
QUERY_EXACT_JOB_IN_PRODUCTION, QUERY_EXACT_JOB_IN_PRODUCTION,
QUERY_EXACT_JOBS_IN_PRODUCTION, QUERY_EXACT_JOBS_IN_PRODUCTION,
@@ -9,19 +9,42 @@ import {
} from "../../graphql/jobs.queries"; } from "../../graphql/jobs.queries";
import ProductionListTable from "./production-list-table.component"; import ProductionListTable from "./production-list-table.component";
import _ from "lodash"; import _ from "lodash";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
export default function ProductionListTableContainer({ subscriptionType = "direct" }) { export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
const client = useApolloClient();
const { socket } = useContext(SocketContext);
const [joblist, setJoblist] = useState([]);
// Get Split treatment
const {
treatments: { Websocket_Production }
} = useSplitTreatments({
attributes: {},
names: ["Websocket_Production"],
splitKey: bodyshop && bodyshop.imexshopid
});
// Determine if subscription is enabled
const subscriptionEnabled = Websocket_Production?.treatment === "off";
// Use GraphQL query
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, { const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
pollInterval: 3600000, pollInterval: 3600000,
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only" nextFetchPolicy: "network-only"
}); });
const client = useApolloClient();
const [joblist, setJoblist] = useState([]); // Use GraphQL subscription when subscription is enabled
const { data: updatedJobs } = useSubscription( const { data: updatedJobs } = useSubscription(
subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION,
{
skip: !subscriptionEnabled
}
); );
// Update joblist when data changes
useEffect(() => { useEffect(() => {
if (!(data && data.jobs)) return; if (!(data && data.jobs)) return;
setJoblist( setJoblist(
@@ -31,34 +54,104 @@ export default function ProductionListTableContainer({ subscriptionType = "direc
); );
}, [data]); }, [data]);
// Handle updates from GraphQL subscription
useEffect(() => { useEffect(() => {
if (!updatedJobs || joblist.length === 0) return; if (subscriptionEnabled) {
if (!updatedJobs || joblist.length === 0) return;
const jobDiff = _.differenceWith( const jobDiff = _.differenceWith(
joblist, joblist,
updatedJobs.jobs, updatedJobs.jobs,
(a, b) => a.id === b.id && a.updated_at === b.updated_at (a, b) => a.id === b.id && a.updated_at === b.updated_at
); );
if (jobDiff.length > 1) { if (jobDiff.length > 1) {
getUpdatedJobsData(jobDiff.map((j) => j.id)); getUpdatedJobsData(jobDiff.map((j) => j.id));
} else if (jobDiff.length === 1) { } else if (jobDiff.length === 1) {
jobDiff.forEach((job) => { jobDiff.forEach((job) => {
getUpdatedJobData(job.id); getUpdatedJobData(job.id);
}); });
}
setJoblist(updatedJobs.jobs);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updatedJobs, subscriptionEnabled]);
// Handle updates from Socket.IO when subscription is disabled
useEffect(() => {
if (subscriptionEnabled || !socket || !bodyshop || !bodyshop.id) {
return;
} }
setJoblist(updatedJobs.jobs); const handleJobUpdates = async (jobChangedData) => {
// eslint-disable-next-line react-hooks/exhaustive-deps const jobId = jobChangedData.id;
}, [updatedJobs]);
// Access the existing cache for QUERY_JOBS_IN_PRODUCTION
const existingJobsCache = client.readQuery({
query: QUERY_JOBS_IN_PRODUCTION
});
const existingJobs = existingJobsCache?.jobs || [];
// Check if the job already exists in the cached jobs
const existingJob = existingJobs.find((job) => job.id === jobId);
if (existingJob) {
// If the job exists, we update the cache without making any additional queries
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: existingJobs.map((job) =>
job.id === jobId ? { ...existingJob, ...jobChangedData, __typename: "jobs" } : job
)
}
});
} else {
// If the job doesn't exist, fetch it from the server and then add it to the cache
try {
const { data: jobData } = await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId },
fetchPolicy: "network-only"
});
// Add the job to the existing cached jobs
client.writeQuery({
query: QUERY_JOBS_IN_PRODUCTION,
data: {
jobs: [...existingJobs, { ...jobData.job, __typename: "jobs" }]
}
});
} catch (error) {
console.error(`Error fetching job ${jobId}: ${error.message}`);
}
}
};
const handleReconnect = () => {
//If we were disconnected from the board, we missed stuff. We need to refresh it entirely.
if (refetch) refetch();
};
// Listen for 'production-job-updated' events
socket.on("production-job-updated", handleJobUpdates);
socket.on("reconnect", handleReconnect);
// Clean up on unmount or when dependencies change
return () => {
socket.off("production-job-updated", handleJobUpdates);
socket.off("reconnect", handleReconnect);
};
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
// Functions to fetch updated job data
const getUpdatedJobData = async (jobId) => { const getUpdatedJobData = async (jobId) => {
client.query({ await client.query({
query: QUERY_EXACT_JOB_IN_PRODUCTION, query: QUERY_EXACT_JOB_IN_PRODUCTION,
variables: { id: jobId } variables: { id: jobId },
fetchPolicy: "network-only"
}); });
}; };
const getUpdatedJobsData = async (jobIds) => { const getUpdatedJobsData = (jobIds) => {
client.query({ client.query({
query: QUERY_EXACT_JOBS_IN_PRODUCTION, query: QUERY_EXACT_JOBS_IN_PRODUCTION,
variables: { ids: jobIds } variables: { ids: jobIds }

View File

@@ -0,0 +1,18 @@
import { connect } from "react-redux";
import { GlobalOutlined } from "@ant-design/icons";
import { createStructuredSelector } from "reselect";
import React from "react";
import { selectWssStatus } from "../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
wssStatus: selectWssStatus
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
export function WssStatusDisplay({ wssStatus }) {
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
return <GlobalOutlined style={{ color: wssStatus === "connected" ? "green" : "red", marginRight: ".5rem" }} />;
}

View File

@@ -0,0 +1,13 @@
import React, { createContext } from "react";
import useSocket from "./useSocket"; // Import the custom hook
// Create the SocketContext
const SocketContext = createContext(null);
export const SocketProvider = ({ children, bodyshop }) => {
const { socket, clientId } = useSocket(bodyshop);
return <SocketContext.Provider value={{ socket, clientId }}> {children}</SocketContext.Provider>;
};
export default SocketContext;

View File

@@ -0,0 +1,88 @@
import { useEffect, useState, useRef } from "react";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
import { store } from "../../redux/store";
import { setWssStatus } from "../../redux/application/application.actions";
const useSocket = (bodyshop) => {
const socketRef = useRef(null);
const [clientId, setClientId] = useState(null);
useEffect(() => {
const unsubscribe = auth.onIdTokenChanged(async (user) => {
if (user) {
const newToken = await user.getIdToken();
if (socketRef.current) {
// Send new token to server
socketRef.current.emit("update-token", newToken);
} else if (bodyshop && bodyshop.id) {
// Initialize the socket
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
const socketInstance = SocketIO(endpoint, {
path: "/wss",
withCredentials: true,
auth: { token: newToken },
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
reconnectionDelayMax: 10000
});
socketRef.current = socketInstance;
const handleBodyshopMessage = (message) => {
if (!import.meta.env.DEV) return;
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
const handleConnect = () => {
console.log("Socket connected:", socketInstance.id);
socketInstance.emit("join-bodyshop-room", bodyshop.id);
setClientId(socketInstance.id);
store.dispatch(setWssStatus("connected"))
};
const handleReconnect = (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
store.dispatch(setWssStatus("connected"))
};
const handleConnectionError = (err) => {
console.error("Socket connection error:", err);
store.dispatch(setWssStatus("error"))
};
const handleDisconnect = () => {
console.log("Socket disconnected");
store.dispatch(setWssStatus("disconnected"))
};
socketInstance.on("connect", handleConnect);
socketInstance.on("reconnect", handleReconnect);
socketInstance.on("connect_error", handleConnectionError);
socketInstance.on("disconnect", handleDisconnect);
socketInstance.on("bodyshop-message", handleBodyshopMessage);
}
} else {
// User is not authenticated
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
}
});
// Clean up the listener on unmount
return () => {
unsubscribe();
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
}
};
}, [bodyshop]);
return { socket: socketRef.current, clientId };
};
export default useSocket;

View File

@@ -23,17 +23,14 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO( export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin, path: "/ws",
{ withCredentials: true,
path: "/ws", auth: async (callback) => {
withCredentials: true, const token = auth.currentUser && (await auth.currentUser.getIdToken());
auth: async (callback) => { callback({ token });
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
} }
); });
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -34,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO( export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing, import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", // for dev testing,
{ {
path: "/ws", path: "/ws",
withCredentials: true, withCredentials: true,

View File

@@ -1,6 +1,6 @@
import { FloatButton, Layout, Spin } from "antd"; import { FloatButton, Layout, Spin } from "antd";
// import preval from "preval.macro"; // import preval from "preval.macro";
import React, { lazy, Suspense, useEffect, useState } from "react"; import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, Route, Routes } from "react-router-dom"; import { Link, Route, Routes } from "react-router-dom";
@@ -19,11 +19,13 @@ import PartnerPingComponent from "../../components/partner-ping/partner-ping.com
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container"; import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component"; import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
import { requestForToken } from "../../firebase/firebase.utils"; import { requestForToken } from "../../firebase/firebase.utils";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors"; import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component"; import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss"; import "./manage.page.styles.scss";
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
const JobsPage = lazy(() => import("../jobs/jobs.page")); const JobsPage = lazy(() => import("../jobs/jobs.page"));
@@ -110,6 +112,7 @@ const mapDispatchToProps = (dispatch) => ({});
export function Manage({ conflict, bodyshop }) { export function Manage({ conflict, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [chatVisible] = useState(false); const [chatVisible] = useState(false);
const { socket, clientId } = useContext(SocketContext);
useEffect(() => { useEffect(() => {
const widgetId = InstanceRenderManager({ const widgetId = InstanceRenderManager({
@@ -129,6 +132,7 @@ export function Manage({ conflict, bodyshop }) {
promanager: t("titles.promanager") promanager: t("titles.promanager")
}); });
}, [t]); }, [t]);
const AppRouteTable = ( const AppRouteTable = (
<Suspense <Suspense
fallback={ fallback={
@@ -569,6 +573,13 @@ export function Manage({ conflict, bodyshop }) {
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />; else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
else PageContent = AppRouteTable; else PageContent = AppRouteTable;
const broadcastMessage = () => {
if (socket && bodyshop && bodyshop.id) {
console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
}
};
return ( return (
<> <>
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />} {import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
@@ -594,7 +605,8 @@ export function Manage({ conflict, bodyshop }) {
}} }}
> >
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div> <WssStatusDisplayComponent />
<div onClick={broadcastMessage}>
{`${InstanceRenderManager({ {`${InstanceRenderManager({
imex: t("titles.imexonline"), imex: t("titles.imexonline"),
rome: t("titles.romeonline"), rome: t("titles.romeonline"),

View File

@@ -26,7 +26,7 @@ export function ProductionListComponent({ bodyshop }) {
return ( return (
<> <>
<NoteUpsertModal /> <NoteUpsertModal />
<ProductionListTable subscriptionType={Production_Use_View.treatment} /> <ProductionListTable bodyshop={bodyshop} subscriptionType={Production_Use_View.treatment} />
</> </>
); );
} }

View File

@@ -67,3 +67,7 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE, type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable payload: isUpdateAvailable
}); });
export const setWssStatus = (status) => ({
type: ApplicationActionTypes.SET_WSS_STATUS,
payload: status
});

View File

@@ -3,6 +3,7 @@ import ApplicationActionTypes from "./application.types";
const INITIAL_STATE = { const INITIAL_STATE = {
loading: false, loading: false,
online: true, online: true,
wssStatus: "disconnected",
updateAvailable: false, updateAvailable: false,
breadcrumbs: [], breadcrumbs: [],
recentItems: [], recentItems: [],
@@ -87,6 +88,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.SET_PROBLEM_JOBS: { case ApplicationActionTypes.SET_PROBLEM_JOBS: {
return { ...state, problemJobs: action.payload }; return { ...state, problemJobs: action.payload };
} }
case ApplicationActionTypes.SET_WSS_STATUS: {
return { ...state, wssStatus: action.payload };
}
default: default:
return state; return state;
} }

View File

@@ -22,3 +22,4 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
export const selectOnline = createSelector([selectApplication], (application) => application.online); export const selectOnline = createSelector([selectApplication], (application) => application.online);
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs); export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable); export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);

View File

@@ -12,6 +12,7 @@ const ApplicationActionTypes = {
SET_ONLINE_STATUS: "SET_ONLINE_STATUS", SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL", INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS", SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE" SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_WSS_STATUS: "SET_WSS_STATUS"
}; };
export default ApplicationActionTypes; export default ApplicationActionTypes;

View File

@@ -242,6 +242,10 @@ export function* signInSuccessSaga({ payload }) {
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]); window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
window.$crisp.push(["set", "session:segments", [["user"]]]); window.$crisp.push(["set", "session:segments", [["user"]]]);
}, },
rome: () => {
window.$zoho.salesiq.visitor.name(payload.displayName || payload.email);
window.$zoho.salesiq.visitor.email(payload.email);
},
promanager: () => { promanager: () => {
Userpilot.identify(payload.email, { Userpilot.identify(payload.email, {
email: payload.email email: payload.email
@@ -371,6 +375,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
if (authRecord[0] && authRecord[0].user.validemail) { if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]); window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
} }
},
rome: () => {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
} }
}); });
} catch (error) { } catch (error) {

View File

@@ -2,9 +2,9 @@ import axios from "axios";
import { auth } from "../firebase/firebase.utils"; import { auth } from "../firebase/firebase.utils";
import InstanceRenderManager from "./instanceRenderMgr"; import InstanceRenderManager from "./instanceRenderMgr";
axios.defaults.baseURL = axios.defaults.baseURL = import.meta.env.DEV
import.meta.env.VITE_APP_AXIOS_BASE_API_URL || ? "/api/"
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/"); : import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
export const axiosAuthInterceptorId = axios.interceptors.request.use( export const axiosAuthInterceptorId = axios.interceptors.request.use(
async (config) => { async (config) => {

View File

@@ -3,16 +3,22 @@ import { promises as fsPromises } from "fs";
import { createRequire } from "module"; import { createRequire } from "module";
import * as path from "path"; import * as path from "path";
import * as url from "url"; import * as url from "url";
import { defineConfig } from "vite"; import { createLogger, defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs"; import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint"; import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr"; import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
//import { visualizer } from "rollup-plugin-visualizer";
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles" timeZone: "America/Los_Angeles"
}); });
const getFormattedTimestamp = () =>
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`; const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
function reactVirtualizedFix() { function reactVirtualizedFix() {
@@ -32,10 +38,16 @@ function reactVirtualizedFix() {
} }
}; };
} }
/** End of hack */
export const logger = createLogger("info", {
allowClearScreen: false
});
export default defineConfig({ export default defineConfig({
base: "/", base: "/",
plugins: [ plugins: [
//visualizer(),
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
VitePWA({ VitePWA({
injectRegister: "auto", injectRegister: "auto",
@@ -99,7 +111,6 @@ export default defineConfig({
reactVirtualizedFix(), reactVirtualizedFix(),
react(), react(),
eslint() eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
], ],
define: { define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version) APP_VERSION: JSON.stringify(process.env.npm_package_version)
@@ -107,7 +118,69 @@ export default defineConfig({
server: { server: {
host: true, host: true,
port: 3000, port: 3000,
open: true open: true,
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/wss": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false,
rewrite: (path) => {
const replacedValue = path.replace(/^\/api/, "");
logger.info(
`${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}`
);
return replacedValue;
}
}
},
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
}
},
preview: {
port: 6000,
host: true,
open: true,
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
},
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/wss": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false
}
}
}, },
build: { build: {
rollupOptions: { rollupOptions: {
@@ -115,17 +188,63 @@ export default defineConfig({
manualChunks: { manualChunks: {
antd: ["antd"], antd: ["antd"],
"react-redux": ["react-redux"], "react-redux": ["react-redux"],
redux: ["redux"] redux: ["redux"],
lodash: ["lodash"],
"@sentry/react": ["@sentry/react"],
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
logrocket: ["logrocket"],
"firebase/app": ["firebase/app"],
"firebase/firestore": ["firebase/firestore"],
"firebase/firestore/lite": ["firebase/firestore/lite"],
"firebase/auth": ["firebase/auth"],
"firebase/functions": ["firebase/functions"],
"firebase/storage": ["firebase/storage"],
"firebase/database": ["firebase/database"],
"firebase/remote-config": ["firebase/remote-config"],
"firebase/performance": ["firebase/performance"],
"@firebase/app": ["@firebase/app"],
"@firebase/firestore": ["@firebase/firestore"],
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
"@firebase/auth": ["@firebase/auth"],
"@firebase/functions": ["@firebase/functions"],
"@firebase/storage": ["@firebase/storage"],
"@firebase/database": ["@firebase/database"],
"@firebase/remote-config": ["@firebase/remote-config"],
"@firebase/performance": ["@firebase/performance"],
markerjs2: ["markerjs2"],
"@apollo/client": ["@apollo/client"],
"libphonenumber-js": ["libphonenumber-js"]
} }
} }
} }
}, },
optimizeDeps: { optimizeDeps: {
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios"], include: [
"react",
"react-dom",
"antd",
"lodash",
"@sentry/react",
"@apollo/client",
"@reduxjs/toolkit",
"axios",
"react-router-dom",
"dayjs",
"redux",
"react-redux"
],
esbuildOptions: { esbuildOptions: {
loader: { loader: {
".js": "jsx" ".js": "jsx"
} }
} }
},
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler",
quietDeps: true // Quite Deprecation Warnings, should be disabled occasionally before major upgrades
}
}
} }
}); });

View File

@@ -1,4 +1,4 @@
Must set the environment variables using: Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql" firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
auth.hasura_secret_admin_key="Dev-BodyShopApp!" auth.hasura_secret_admin_key="Dev-BodyShopApp!"

1597
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,26 +19,28 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"" "makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.629.0", "@aws-sdk/client-secrets-manager": "^3.654.0",
"@aws-sdk/client-ses": "^3.629.0", "@aws-sdk/client-ses": "^3.654.0",
"@aws-sdk/credential-provider-node": "^3.629.0", "@aws-sdk/credential-provider-node": "^3.654.0",
"@opensearch-project/opensearch": "^2.11.0", "@opensearch-project/opensearch": "^2.12.0",
"aws4": "^1.13.1", "@socket.io/admin-ui": "^0.5.1",
"axios": "^1.7.4", "@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
"better-queue": "^3.8.12", "better-queue": "^3.8.12",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.3",
"canvas": "^2.11.2", "canvas": "^2.11.2",
"chart.js": "^4.4.3", "chart.js": "^4.4.4",
"cloudinary": "^2.4.0", "cloudinary": "^2.5.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "2.8.5", "cors": "2.8.5",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.21.0",
"firebase-admin": "^12.3.1", "firebase-admin": "^12.5.0",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"graylog2": "^0.2.1", "graylog2": "^0.2.1",
@@ -49,14 +51,16 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.45", "moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.5", "node-mailjet": "^6.0.6",
"node-persist": "^4.0.3", "node-persist": "^4.0.3",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.15",
"phone": "^3.1.49", "phone": "^3.1.50",
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"soap": "^1.1.1", "soap": "^1.1.4",
"socket.io": "^4.7.5", "socket.io": "^4.8.0",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^10.0.3", "ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0", "twilio": "^4.23.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",

272
server.js
View File

@@ -1,4 +1,3 @@
// Import core modules
const express = require("express"); const express = require("express");
const cors = require("cors"); const cors = require("cors");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
@@ -7,104 +6,209 @@ const compression = require("compression");
const cookieParser = require("cookie-parser"); const cookieParser = require("cookie-parser");
const http = require("http"); const http = require("http");
const { Server } = require("socket.io"); const { Server } = require("socket.io");
const { createClient } = require("redis");
const { createAdapter } = require("@socket.io/redis-adapter");
const logger = require("./server/utils/logger");
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
const { instrument, RedisStore } = require("@socket.io/admin-ui");
const { isString, isEmpty } = require("lodash");
const applyRedisHelpers = require("./server/utils/redisHelpers");
const applyIOHelpers = require("./server/utils/ioHelpers");
// Load environment variables // Load environment variables
require("dotenv").config({ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
}); });
// Import custom utilities and handlers /**
const logger = require("./server/utils/logger"); * CORS Origin for Socket.IO
* @type {string[][]}
*/
const SOCKETIO_CORS_ORIGIN = [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io",
"https://www.romeonline.io",
"https://test.romeonline.io",
"https://www.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://old.imex.online",
"https://www.old.imex.online",
"https://wsadmin.imex.online",
"https://www.wsadmin.imex.online",
"http://localhost:3333",
"https://localhost:3333"
];
// Express app and server setup /**
const app = express(); * Middleware for Express app
const port = process.env.PORT || 5000; * @param app
const server = http.createServer(app); */
const io = new Server(server, { const applyMiddleware = (app) => {
path: "/ws", app.use(compression());
cors: { app.use(cookieParser());
origin: [ app.use(bodyParser.json({ limit: "50mb" }));
"https://test.imex.online", app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
"https://www.test.imex.online", app.use(
"http://localhost:3000", cors({
"https://imex.online", origin: SOCKETIO_CORS_ORIGIN,
"https://www.imex.online", credentials: true,
"https://romeonline.io", //Added in all RO and PM routes to simplyify setup. exposedHeaders: ["set-cookie"]
"https://www.romeonline.io", })
"https://beta.test.romeonline.io", );
"https://www.beta.test.romeonline.io", // Helper middleware
"https://beta.romeonline.io", app.use((req, res, next) => {
"https://www.beta.romeonline.io", req.logger = logger;
"https://beta.test.imex.online", next();
"https://www.beta.test.imex.online", });
"https://beta.imex.online", };
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com", /**
"https://test.promanager.web-est.com", * Route groupings for Express app
"https://www.promanager.web-est.com", * @param app
"https://www.promanager.web-est.com" */
], const applyRoutes = (app) => {
methods: ["GET", "POST"], app.use("/", require("./server/routes/miscellaneousRoutes"));
credentials: true, app.use("/notifications", require("./server/routes/notificationsRoutes"));
exposedHeaders: ["set-cookie"] app.use("/render", require("./server/routes/renderRoutes"));
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
app.use("/accounting", require("./server/routes/accountingRoutes"));
app.use("/qbo", require("./server/routes/qboRoutes"));
app.use("/media", require("./server/routes/mediaRoutes"));
app.use("/sms", require("./server/routes/smsRoutes"));
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
};
/**
* Apply Redis to the server
* @param server
* @param app
*/
const applySocketIO = async (server, app) => {
// Redis client setup for Pub/Sub and Key-Value Store
const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
const subClient = pubClient.duplicate();
pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis"));
subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis"));
try {
await Promise.all([pubClient.connect(), subClient.connect()]);
logger.log(`[${process.env.NODE_ENV}] Connected to Redis`, "INFO", "redis", "api");
} catch (redisError) {
logger.log("Failed to connect to Redis", "ERROR", "redis", redisError);
} }
});
exports.io = io;
require("./server/web-sockets/web-socket"); process.on("SIGINT", async () => {
logger.log("Closing Redis connections...", "INFO", "redis", "api");
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
process.exit(0);
});
// Middleware const ioRedis = new Server(server, {
app.use(compression()); path: "/wss",
app.use(cookieParser()); adapter: createAdapter(pubClient, subClient),
app.use(bodyParser.json({ limit: "50mb" })); cors: {
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); origin: SOCKETIO_CORS_ORIGIN,
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] })); methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
// Helper middleware if (isString(process.env.REDIS_ADMIN_PASS) && !isEmpty(process.env.REDIS_ADMIN_PASS)) {
app.use((req, res, next) => { logger.log(`[${process.env.NODE_ENV}] Initializing Redis Admin UI....`, "INFO", "redis", "api");
req.logger = logger; instrument(ioRedis, {
next(); auth: {
}); type: "basic",
username: "admin",
password: process.env.REDIS_ADMIN_PASS
},
mode: process.env.REDIS_ADMIN_MODE || "development"
});
}
// Route groupings const io = new Server(server, {
app.use("/", require("./server/routes/miscellaneousRoutes")); path: "/ws",
app.use("/notifications", require("./server/routes/notificationsRoutes")); cors: {
app.use("/render", require("./server/routes/renderRoutes")); origin: SOCKETIO_CORS_ORIGIN,
app.use("/mixdata", require("./server/routes/mixDataRoutes")); methods: ["GET", "POST"],
app.use("/accounting", require("./server/routes/accountingRoutes")); credentials: true,
app.use("/qbo", require("./server/routes/qboRoutes")); exposedHeaders: ["set-cookie"]
app.use("/media", require("./server/routes/mediaRoutes")); }
app.use("/sms", require("./server/routes/smsRoutes")); });
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
// Default route for forbidden access app.use((req, res, next) => {
app.get("/", (req, res) => { Object.assign(req, {
res.status(200).send("Access Forbidden."); pubClient,
}); io,
ioRedis
});
next();
});
Object.assign(module.exports, { io, pubClient, ioRedis });
return { pubClient, io, ioRedis };
};
/**
* Main function to start the server
* @returns {Promise<void>}
*/
const main = async () => { const main = async () => {
await server.listen(port); const app = express();
const port = process.env.PORT || 5000;
const server = http.createServer(app);
const { pubClient, ioRedis } = await applySocketIO(server, app);
const api = applyRedisHelpers(pubClient, app);
const ioHelpers = applyIOHelpers(app, api, ioRedis, logger);
// Legacy Socket Events
require("./server/web-sockets/web-socket");
applyMiddleware(app);
applyRoutes(app);
redisSocketEvents(ioRedis, api, ioHelpers);
try {
await server.listen(port);
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
} catch (error) {
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
}
}; };
// Start server // Start server
main() main();
.then(() => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api");
})
.catch((error) => {
logger.log(
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`,
"ERROR",
"api",
error
);
});

View File

@@ -7,9 +7,14 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
}); });
// Emit this to bodyshop room
exports.default = async (req, res) => { exports.default = async (req, res) => {
const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body; const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body;
const {
ioRedis,
ioHelpers: { getBodyshopRoom }
} = req;
try { try {
// await client.request(queries.INSERT_IOEVENT, { // await client.request(queries.INSERT_IOEVENT, {
// event: { // event: {
@@ -22,6 +27,12 @@ exports.default = async (req, res) => {
// useremail // useremail
// } // }
// }); // });
ioRedis.to(getBodyshopRoom(bodyshopid)).emit("bodyshop-message", {
operationName,
useremail
});
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
logger.log("ioevent-error", "trace", user, null, { logger.log("ioevent-error", "trace", user, null, {

32
server/job/job-updated.js Normal file
View File

@@ -0,0 +1,32 @@
const { isObject } = require("lodash");
const jobUpdated = async (req, res) => {
const { ioRedis, logger, ioHelpers } = req;
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
logger.log("job-update-error", "ERROR", req.user?.email, null, {
message: `Malformed Job Update request sent from Hasura`,
body: req?.body
});
return res.json({
status: "error",
message: `Malformed Job Update request sent from Hasura`
});
}
logger.log("job-update", "INFO", req.user?.email, null, {
message: `Job updated event received from Hasura`,
jobid: req?.body?.event?.data?.new?.id
});
const updatedJob = req.body.event.data.new;
const bodyshopID = updatedJob.shopid;
// Emit the job-updated event only to the room corresponding to the bodyshop
ioRedis.to(ioHelpers.getBodyshopRoom(bodyshopID)).emit("production-job-updated", updatedJob);
return res.json({ message: "Job updated and event emitted" });
};
module.exports = jobUpdated;

View File

@@ -14,3 +14,4 @@ exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti; exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition; exports.statustransition = require("./job-status-transition").statustransition;
exports.lifecycle = require("./job-lifecycle"); exports.lifecycle = require("./job-lifecycle");
exports.jobUpdated = require("./job-updated");

View File

@@ -5,7 +5,7 @@ const ppc = require("../ccc/partspricechange");
const { partsScan } = require("../parts-scan/parts-scan"); const { partsScan } = require("../parts-scan/parts-scan");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware"); const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti } = require("../job/job"); const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti, jobUpdated } = require("../job/job");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals); router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
@@ -16,5 +16,6 @@ router.post("/lifecycle", validateFirebaseIdTokenMiddleware, withUserGraphQLClie
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti); router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan); router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc); router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
router.post("/job-updated", eventAuthorizationMiddleware, jobUpdated);
module.exports = router; module.exports = router;

17
server/utils/ioHelpers.js Normal file
View File

@@ -0,0 +1,17 @@
const applyIOHelpers = (app, api, io, logger) => {
const getBodyshopRoom = (bodyshopID) => `broadcast-room-${bodyshopID}`;
const ioHelpersAPI = {
getBodyshopRoom
};
// Helper middleware
app.use((req, res, next) => {
req.ioHelpers = ioHelpersAPI;
next();
});
return ioHelpersAPI;
};
module.exports = applyIOHelpers;

View File

@@ -0,0 +1,189 @@
/**
* Apply Redis helper functions
* @param pubClient
* @param app
*/
const applyRedisHelpers = (pubClient, app) => {
// Store session data in Redis
const setSessionData = async (socketId, key, value) => {
await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
};
// Retrieve session data from Redis
const getSessionData = async (socketId, key) => {
const data = await pubClient.hGet(`socket:${socketId}`, key);
return data ? JSON.parse(data) : null;
};
// Clear session data from Redis
const clearSessionData = async (socketId) => {
await pubClient.del(`socket:${socketId}`);
};
// Store multiple session data in Redis
const setMultipleSessionData = async (socketId, keyValues) => {
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
};
// Retrieve multiple session data from Redis
const getMultipleSessionData = async (socketId, keys) => {
const data = await pubClient.hmGet(`socket:${socketId}`, keys);
// Redis returns an object with null values for missing keys, so we parse the non-null ones
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
};
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
// Use Redis multi/pipeline to batch the commands
const multi = pubClient.multi();
keyValueArray.forEach(([key, value]) => {
multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
});
await multi.exec(); // Execute all queued commands
};
// Helper function to add an item to the end of the Redis list
const addItemToEndOfList = async (socketId, key, newItem) => {
try {
await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
}
};
// Helper function to add an item to the beginning of the Redis list
const addItemToBeginningOfList = async (socketId, key, newItem) => {
try {
await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
}
};
// Helper function to clear a list in Redis
const clearList = async (socketId, key) => {
try {
await pubClient.del(`socket:${socketId}:${key}`);
} catch (error) {
console.error(`Error clearing list for socket ${socketId}:`, error);
}
};
// Add methods to manage room users
const addUserToRoom = async (bodyshopUUID, user) => {
try {
await pubClient.sAdd(`bodyshopRoom:${bodyshopUUID}`, JSON.stringify(user));
} catch (err) {
console.error(`Error adding user to room: ${bodyshopUUID}`);
}
};
const removeUserFromRoom = async (bodyshopUUID, user) => {
try {
await pubClient.sRem(`bodyshopRoom:${bodyshopUUID}`, JSON.stringify(user));
} catch (err) {
console.error(`Error remove user from room: ${bodyshopUUID}`);
}
};
const getUsersInRoom = async (bodyshopUUID) => {
try {
const users = await pubClient.sMembers(`bodyshopRoom:${bodyshopUUID}`);
return users.map((user) => JSON.parse(user));
} catch (err) {
console.error(`Error getUsersInRoom: ${bodyshopUUID}`);
}
};
const api = {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
clearList,
addUserToRoom,
removeUserFromRoom,
getUsersInRoom
};
Object.assign(module.exports, api);
app.use((req, res, next) => {
req.sessionUtils = api;
next();
});
// // Demo to show how all the helper functions work
// const demoSessionData = async () => {
// const socketId = "testSocketId";
//
// // Store session data using setSessionData
// await exports.setSessionData(socketId, "field1", "Hello, Redis!");
//
// // Retrieve session data using getSessionData
// const field1Value = await exports.getSessionData(socketId, "field1");
// console.log("Retrieved single field value:", field1Value);
//
// // Store multiple session data using setMultipleSessionData
// await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
//
// // Retrieve multiple session data using getMultipleSessionData
// const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
// console.log("Retrieved multiple field values:", multipleFields);
//
// // Store multiple session data using setMultipleFromArraySessionData
// await exports.setMultipleFromArraySessionData(socketId, [
// ["field4", "Fourth Value"],
// ["field5", "Fifth Value"]
// ]);
//
// // Retrieve and log all fields
// const allFields = await exports.getMultipleSessionData(socketId, [
// "field1",
// "field2",
// "field3",
// "field4",
// "field5"
// ]);
// console.log("Retrieved all field values:", allFields);
//
// // Add item to the end of a Redis list
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
//
// // Add item to the beginning of a Redis list
// await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
//
// // Retrieve the entire list (using lRange)
// const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List:", logEvents.map(JSON.parse));
//
// // **Add the new code below to test clearList**
//
// // Clear the list using clearList
// await exports.clearList(socketId, "logEvents");
// console.log("Log Events List cleared.");
//
// // Retrieve the list after clearing to confirm it's empty
// const logEventsAfterClear = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array
//
// // Clear session data
// await exports.clearSessionData(socketId);
// console.log("Session data cleared.");
// };
//
// if (process.env.NODE_ENV === "development") {
// demoSessionData();
// }
// "th1s1sr3d1s" (BCrypt)
return api;
};
module.exports = applyRedisHelpers;

View File

@@ -0,0 +1,117 @@
const path = require("path");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const logger = require("../utils/logger");
const { admin } = require("../firebase/firebase-handler");
// Logging helper functions
function createLogEvent(socket, level, message) {
console.log(`[IOREDIS LOG EVENT] - ${socket.user.email} - ${socket.id} - ${message}`);
logger.log("ioredis-log-event", level, socket.user.email, null, { wsmessage: message });
}
const registerUpdateEvents = (socket) => {
socket.on("update-token", async (newToken) => {
try {
socket.user = await admin.auth().verifyIdToken(newToken);
createLogEvent(socket, "INFO", "Token updated successfully");
socket.emit("token-updated", { success: true });
} catch (error) {
createLogEvent(socket, "ERROR", `Token update failed: ${error.message}`);
socket.emit("token-updated", { success: false, error: error.message });
// Optionally disconnect the socket if token verification fails
socket.disconnect();
}
});
};
const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }, { getBodyshopRoom }) => {
// Room management and broadcasting events
function registerRoomAndBroadcastEvents(socket) {
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
const room = getBodyshopRoom(bodyshopUUID);
socket.join(room);
await addUserToRoom(room, { uid: socket.user.uid, email: socket.user.email });
createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
// Notify all users in the room about the updated user list
const usersInRoom = await getUsersInRoom(bodyshopUUID);
io.to(room).emit("room-users-updated", usersInRoom);
});
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
const room = getBodyshopRoom(bodyshopUUID);
socket.leave(room);
createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${room}`);
});
socket.on("get-room-users", async (bodyshopUUID, callback) => {
const usersInRoom = await getUsersInRoom(getBodyshopRoom(bodyshopUUID));
callback(usersInRoom);
});
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
const room = getBodyshopRoom(bodyshopUUID);
io.to(room).emit("bodyshop-message", message);
createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${room}`);
});
socket.on("disconnect", async () => {
createLogEvent(socket, "DEBUG", `User disconnected.`);
// Get all rooms the socket is part of
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
for (const bodyshopRoom of rooms) {
await removeUserFromRoom(bodyshopRoom, { uid: socket.user.uid, email: socket.user.email });
// Notify all users in the room about the updated user list
const usersInRoom = await getUsersInRoom(bodyshopRoom);
io.to(bodyshopRoom).emit("room-users-updated", usersInRoom);
}
});
}
// Register all socket events for a given socket connection
function registerSocketEvents(socket) {
createLogEvent(socket, "DEBUG", `Connected and Authenticated.`);
// Register room and broadcasting events
registerRoomAndBroadcastEvents(socket);
registerUpdateEvents(socket);
}
const authMiddleware = (socket, next) => {
try {
if (socket.handshake.auth.token) {
admin
.auth()
.verifyIdToken(socket.handshake.auth.token)
.then((user) => {
socket.user = user;
next();
})
.catch((error) => {
next(new Error(`Authentication error: ${error.message}`));
});
} else {
next(new Error("Authentication error - no authorization token."));
}
} catch (error) {
console.log("Uncaught connection error:::", error);
logger.log("websocket-connection-error", "error", null, null, {
...error
});
next(new Error(`Authentication error ${error}`));
}
};
// Socket.IO Middleware and Connection
io.use(authMiddleware);
io.on("connection", registerSocketEvents);
};
module.exports = {
redisSocketEvents
};