Merge pull request 'Merge latest updates into main' (#2) from dev into main
Some checks failed
Deploy Frontend / deploy (push) Failing after 26s
Some checks failed
Deploy Frontend / deploy (push) Failing after 26s
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
certificate (5).zip
|
certificate (5).zip
|
||||||
|
.env.dev
|
||||||
|
.env.prod
|
||||||
|
|||||||
51
ENV_FILES.md
Normal file
51
ENV_FILES.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Environment Files Setup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Environment variables are stored securely in `/etc/frontend/` and accessed via symlinks in this directory.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
```
|
||||||
|
/etc/frontend/
|
||||||
|
├── dev.env (systemd environment for dev)
|
||||||
|
└── prod.env (systemd environment for prod)
|
||||||
|
|
||||||
|
/home/sonder/ld-sysinfo-react-frontend/
|
||||||
|
├── .env.dev -> /etc/frontend/dev.env (symlink)
|
||||||
|
└── .env.prod -> /etc/frontend/prod.env (symlink)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Editing Environment Variables
|
||||||
|
|
||||||
|
You can edit these files directly from VSCode:
|
||||||
|
- `.env.dev` - Development environment (port 3002)
|
||||||
|
- `.env.prod` - Production environment (port 3001)
|
||||||
|
|
||||||
|
**Note:** You need to be in the `frontend-devs` group to edit these files. Both `sonder` and `deploy` users are members.
|
||||||
|
|
||||||
|
## After Editing
|
||||||
|
|
||||||
|
After making changes to environment files, restart the relevant service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For development
|
||||||
|
sudo systemctl restart frontend@dev
|
||||||
|
|
||||||
|
# For production
|
||||||
|
sudo systemctl restart frontend@prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Both files contain:
|
||||||
|
- `PORT` - Server port (3002 for dev, 3001 for prod)
|
||||||
|
- `API_BASE_URL` - Server-side API endpoint
|
||||||
|
- `NEXT_PUBLIC_API_BASE_URL` - Client-side API endpoint
|
||||||
|
- `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` - Database credentials
|
||||||
|
- `NVD_API_KEY` - API key for CVE data
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Files are stored in `/etc/frontend/` with `660` permissions (owner: root, group: frontend-devs)
|
||||||
|
- Only root and members of `frontend-devs` group can read/write
|
||||||
|
- Files are **not** committed to git (listed in `.gitignore`)
|
||||||
|
- Symlinks are safe to commit as they don't contain sensitive data
|
||||||
221
package-lock.json
generated
221
package-lock.json
generated
@@ -19,8 +19,10 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsonstream": "^1.0.3",
|
"jsonstream": "^1.0.3",
|
||||||
|
"jspdf": "^3.0.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"mysql2": "^3.14.0",
|
"mysql2": "^3.14.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/jspdf": "^1.3.3",
|
||||||
"@types/jwt-decode": "^3.1.0",
|
"@types/jwt-decode": "^3.1.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
@@ -2474,6 +2477,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jspdf": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-DqwyAKpVuv+7DniCp2Deq1xGvfdnKSNgl9Agun2w6dFvR5UKamiv4VfYUgcypd8S9ojUyARFIlZqBrYrBMQlew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/jwt-decode": {
|
"node_modules/@types/jwt-decode": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-3.1.0.tgz",
|
||||||
@@ -2496,6 +2506,12 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||||
@@ -2508,6 +2524,13 @@
|
|||||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.12",
|
"version": "19.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
|
||||||
@@ -2546,6 +2569,13 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.29.0",
|
"version": "8.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz",
|
||||||
@@ -3543,6 +3573,15 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -3688,6 +3727,33 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -3822,6 +3888,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.46.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
|
||||||
|
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||||
@@ -3885,6 +3963,15 @@
|
|||||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -4091,6 +4178,16 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optional": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@@ -4847,6 +4944,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-png": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/pako": "^2.0.3",
|
||||||
|
"iobuffer": "^5.3.2",
|
||||||
|
"pako": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
@@ -4873,6 +4981,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -5316,6 +5430,19 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -5388,6 +5515,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iobuffer": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -5948,6 +6081,23 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.9",
|
||||||
|
"fast-png": "^6.2.0",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.11",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
"version": "3.3.5",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||||
@@ -6854,6 +7004,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -6931,6 +7087,13 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/performance-now": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -7085,6 +7248,16 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
@@ -7337,6 +7510,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -7707,6 +7890,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stream": {
|
"node_modules/stream": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz",
|
||||||
@@ -7922,6 +8115,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.0.tgz",
|
||||||
@@ -7937,6 +8140,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/through": {
|
"node_modules/through": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
@@ -8358,6 +8570,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@@ -23,8 +23,10 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsonstream": "^1.0.3",
|
"jsonstream": "^1.0.3",
|
||||||
|
"jspdf": "^3.0.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"mysql2": "^3.14.0",
|
"mysql2": "^3.14.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/jspdf": "^1.3.3",
|
||||||
"@types/jwt-decode": "^3.1.0",
|
"@types/jwt-decode": "^3.1.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface UserDTO {
|
|||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
|
clientId?: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
484
src/app/(protected)/reporting/page.tsx
Normal file
484
src/app/(protected)/reporting/page.tsx
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Divider,
|
||||||
|
Chip,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
PictureAsPdf as PdfIcon,
|
||||||
|
Assessment as AssessmentIcon,
|
||||||
|
Security as SecurityIcon,
|
||||||
|
Devices as DevicesIcon,
|
||||||
|
Apps as AppsIcon
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import api from '@/lib/axios';
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
interface ComplianceSummary {
|
||||||
|
totalDevices: number;
|
||||||
|
vulnerableDevices: number;
|
||||||
|
totalVulnerabilities: number;
|
||||||
|
criticalVulns: number;
|
||||||
|
highVulns: number;
|
||||||
|
mediumVulns: number;
|
||||||
|
lowVulns: number;
|
||||||
|
totalSoftware: number;
|
||||||
|
vulnerableSoftware: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopVulnerability {
|
||||||
|
cveId: string;
|
||||||
|
title: string;
|
||||||
|
severity: string;
|
||||||
|
score: number;
|
||||||
|
affectedDevices: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TopVulnerableSoftware {
|
||||||
|
softwareName: string;
|
||||||
|
totalInstances: number;
|
||||||
|
vulnerableInstances: number;
|
||||||
|
totalCves: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReportingPage() {
|
||||||
|
const [summary, setSummary] = useState<ComplianceSummary | null>(null);
|
||||||
|
const [topVulns, setTopVulns] = useState<TopVulnerability[]>([]);
|
||||||
|
const [topSoftware, setTopSoftware] = useState<TopVulnerableSoftware[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const reportRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const fetchReportingData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Fetch compliance summary data (you'll need to create these endpoints)
|
||||||
|
const [summaryRes, vulnsRes, softwareRes] = await Promise.all([
|
||||||
|
api.get('/reporting/compliance-summary'),
|
||||||
|
api.get('/reporting/top-vulnerabilities'),
|
||||||
|
api.get('/reporting/vulnerable-software')
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSummary(summaryRes.data);
|
||||||
|
setTopVulns(vulnsRes.data);
|
||||||
|
setTopSoftware(softwareRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch reporting data:', error);
|
||||||
|
// Create mock data for now since endpoints don't exist yet
|
||||||
|
createMockData();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReportingData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createMockData = () => {
|
||||||
|
setSummary({
|
||||||
|
totalDevices: 127,
|
||||||
|
vulnerableDevices: 89,
|
||||||
|
totalVulnerabilities: 342,
|
||||||
|
criticalVulns: 23,
|
||||||
|
highVulns: 67,
|
||||||
|
mediumVulns: 156,
|
||||||
|
lowVulns: 96,
|
||||||
|
totalSoftware: 1247,
|
||||||
|
vulnerableSoftware: 89,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
setTopVulns([
|
||||||
|
{ cveId: 'CVE-2024-12345', title: 'Critical Remote Code Execution', severity: 'CRITICAL', score: 9.8, affectedDevices: 34 },
|
||||||
|
{ cveId: 'CVE-2024-12346', title: 'Privilege Escalation Vulnerability', severity: 'HIGH', score: 8.4, affectedDevices: 28 },
|
||||||
|
{ cveId: 'CVE-2024-12347', title: 'Information Disclosure', severity: 'HIGH', score: 7.9, affectedDevices: 22 },
|
||||||
|
{ cveId: 'CVE-2024-12348', title: 'Cross-Site Scripting', severity: 'MEDIUM', score: 6.1, affectedDevices: 19 },
|
||||||
|
{ cveId: 'CVE-2024-12349', title: 'Denial of Service', severity: 'MEDIUM', score: 5.8, affectedDevices: 15 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
setTopSoftware([
|
||||||
|
{ softwareName: 'Apache HTTP Server', totalInstances: 45, vulnerableInstances: 23, totalCves: 12 },
|
||||||
|
{ softwareName: 'OpenSSL', totalInstances: 89, vulnerableInstances: 34, totalCves: 8 },
|
||||||
|
{ softwareName: 'Microsoft Office', totalInstances: 127, vulnerableInstances: 67, totalCves: 15 },
|
||||||
|
{ softwareName: 'Adobe Reader', totalInstances: 98, vulnerableInstances: 45, totalCves: 9 },
|
||||||
|
{ softwareName: 'Java Runtime Environment', totalInstances: 76, vulnerableInstances: 28, totalCves: 6 }
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportToPDF = async () => {
|
||||||
|
if (!reportRef.current || !summary) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setExporting(true);
|
||||||
|
|
||||||
|
// Create a new jsPDF instance
|
||||||
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||||
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
|
||||||
|
// Add header
|
||||||
|
pdf.setFontSize(20);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Compliance Security Report', pageWidth / 2, 20, { align: 'center' });
|
||||||
|
|
||||||
|
pdf.setFontSize(12);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
pdf.text(`Generated: ${format(new Date(), 'PPP')}`, pageWidth / 2, 30, { align: 'center' });
|
||||||
|
|
||||||
|
let yPosition = 45;
|
||||||
|
|
||||||
|
// Executive Summary
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Executive Summary', 20, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
pdf.setFontSize(10);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
const summaryText = [
|
||||||
|
`Total Devices: ${summary.totalDevices}`,
|
||||||
|
`Vulnerable Devices: ${summary.vulnerableDevices} (${((summary.vulnerableDevices / summary.totalDevices) * 100).toFixed(1)}%)`,
|
||||||
|
`Total Vulnerabilities: ${summary.totalVulnerabilities}`,
|
||||||
|
`Critical: ${summary.criticalVulns} | High: ${summary.highVulns} | Medium: ${summary.mediumVulns} | Low: ${summary.lowVulns}`,
|
||||||
|
`Software Applications: ${summary.totalSoftware}`,
|
||||||
|
`Vulnerable Software: ${summary.vulnerableSoftware}`
|
||||||
|
];
|
||||||
|
|
||||||
|
summaryText.forEach(text => {
|
||||||
|
pdf.text(text, 20, yPosition);
|
||||||
|
yPosition += 6;
|
||||||
|
});
|
||||||
|
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Top Vulnerabilities
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Top Critical Vulnerabilities', 20, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
pdf.setFontSize(9);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
topVulns.slice(0, 10).forEach((vuln, index) => {
|
||||||
|
if (yPosition > pageHeight - 30) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.text(`${index + 1}. ${vuln.cveId} - ${vuln.title}`, 20, yPosition);
|
||||||
|
yPosition += 5;
|
||||||
|
pdf.text(` Severity: ${vuln.severity} | Score: ${vuln.score} | Affected: ${vuln.affectedDevices} devices`, 20, yPosition);
|
||||||
|
yPosition += 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add page break if needed
|
||||||
|
if (yPosition > pageHeight - 80) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = 20;
|
||||||
|
} else {
|
||||||
|
yPosition += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top Vulnerable Software
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Most Vulnerable Software', 20, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
pdf.setFontSize(9);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
topSoftware.slice(0, 10).forEach((software, index) => {
|
||||||
|
if (yPosition > pageHeight - 30) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.text(`${index + 1}. ${software.softwareName}`, 20, yPosition);
|
||||||
|
yPosition += 5;
|
||||||
|
pdf.text(` ${software.vulnerableInstances}/${software.totalInstances} instances vulnerable | ${software.totalCves} CVEs`, 20, yPosition);
|
||||||
|
yPosition += 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
const totalPages = pdf.internal.pages.length - 1;
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pdf.setPage(i);
|
||||||
|
pdf.setFontSize(8);
|
||||||
|
pdf.text(`Page ${i} of ${totalPages}`, pageWidth - 30, pageHeight - 10);
|
||||||
|
pdf.text('Confidential - Internal Use Only', 20, pageHeight - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the PDF
|
||||||
|
const filename = `compliance-report-${format(new Date(), 'yyyy-MM-dd')}.pdf`;
|
||||||
|
pdf.save(filename);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export PDF:', error);
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSeverityColor = (severity: string) => {
|
||||||
|
switch (severity?.toLowerCase()) {
|
||||||
|
case 'critical': return 'error';
|
||||||
|
case 'high': return 'warning';
|
||||||
|
case 'medium': return 'info';
|
||||||
|
case 'low': return 'success';
|
||||||
|
default: return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="60vh">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summary) {
|
||||||
|
return (
|
||||||
|
<Box p={4}>
|
||||||
|
<Alert severity="error">Failed to load reporting data. Please try again later.</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p={4} ref={reportRef}>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={4}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
<AssessmentIcon sx={{ mr: 2, verticalAlign: 'middle' }} />
|
||||||
|
Compliance Reporting
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
|
Comprehensive security compliance overview and vulnerability reporting
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={exporting ? <CircularProgress size={20} /> : <PdfIcon />}
|
||||||
|
onClick={exportToPDF}
|
||||||
|
disabled={exporting}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{exporting ? 'Generating...' : 'Export PDF'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Executive Summary Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" alignItems="center" mb={2}>
|
||||||
|
<DevicesIcon color="primary" sx={{ mr: 1 }} />
|
||||||
|
<Typography variant="h6">Devices</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h3" color="primary">
|
||||||
|
{summary.totalDevices}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{summary.vulnerableDevices} vulnerable ({((summary.vulnerableDevices / summary.totalDevices) * 100).toFixed(1)}%)
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" alignItems="center" mb={2}>
|
||||||
|
<SecurityIcon color="error" sx={{ mr: 1 }} />
|
||||||
|
<Typography variant="h6">Vulnerabilities</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h3" color="error">
|
||||||
|
{summary.totalVulnerabilities}
|
||||||
|
</Typography>
|
||||||
|
<Box display="flex" gap={1} mt={1} flexWrap="wrap">
|
||||||
|
<Chip label={`Critical: ${summary.criticalVulns}`} color="error" size="small" />
|
||||||
|
<Chip label={`High: ${summary.highVulns}`} color="warning" size="small" />
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" alignItems="center" mb={2}>
|
||||||
|
<AppsIcon color="info" sx={{ mr: 1 }} />
|
||||||
|
<Typography variant="h6">Software</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h3" color="info">
|
||||||
|
{summary.totalSoftware}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{summary.vulnerableSoftware} with vulnerabilities
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 3 }}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" mb={2}>Risk Level</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
color={summary.criticalVulns > 0 ? "error" : summary.highVulns > 10 ? "warning" : "success"}
|
||||||
|
>
|
||||||
|
{summary.criticalVulns > 0 ? "HIGH" : summary.highVulns > 10 ? "MEDIUM" : "LOW"}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Last updated: {format(new Date(summary.lastUpdated), 'PPp')}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Detailed Reports */}
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid size={{ xs: 12, lg: 6 }}>
|
||||||
|
<Paper sx={{ p: 3, height: 'fit-content' }}>
|
||||||
|
<Typography variant="h6" mb={3}>
|
||||||
|
<SecurityIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Top Critical Vulnerabilities
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell><strong>CVE ID</strong></TableCell>
|
||||||
|
<TableCell><strong>Severity</strong></TableCell>
|
||||||
|
<TableCell align="right"><strong>Score</strong></TableCell>
|
||||||
|
<TableCell align="right"><strong>Devices</strong></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{topVulns.slice(0, 8).map((vuln) => (
|
||||||
|
<TableRow key={vuln.cveId} hover>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" fontFamily="monospace">
|
||||||
|
{vuln.cveId}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{vuln.title.length > 40 ? `${vuln.title.substring(0, 40)}...` : vuln.title}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
label={vuln.severity}
|
||||||
|
color={getSeverityColor(vuln.severity) as "error" | "warning" | "info" | "success" | "default"}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Typography fontWeight="bold">
|
||||||
|
{vuln.score}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{vuln.affectedDevices}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, lg: 6 }}>
|
||||||
|
<Paper sx={{ p: 3, height: 'fit-content' }}>
|
||||||
|
<Typography variant="h6" mb={3}>
|
||||||
|
<AppsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||||
|
Most Vulnerable Software
|
||||||
|
</Typography>
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell><strong>Software</strong></TableCell>
|
||||||
|
<TableCell align="center"><strong>Vulnerable</strong></TableCell>
|
||||||
|
<TableCell align="center"><strong>CVEs</strong></TableCell>
|
||||||
|
<TableCell align="center"><strong>Risk</strong></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{topSoftware.slice(0, 8).map((software) => {
|
||||||
|
const riskLevel = software.vulnerableInstances / software.totalInstances;
|
||||||
|
return (
|
||||||
|
<TableRow key={software.softwareName} hover>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{software.softwareName}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Typography variant="body2">
|
||||||
|
{software.vulnerableInstances}/{software.totalInstances}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
({((riskLevel) * 100).toFixed(0)}%)
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Typography fontWeight="bold" color="error">
|
||||||
|
{software.totalCves}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Chip
|
||||||
|
label={riskLevel > 0.5 ? "HIGH" : riskLevel > 0.2 ? "MEDIUM" : "LOW"}
|
||||||
|
color={riskLevel > 0.5 ? "error" : riskLevel > 0.2 ? "warning" : "success"}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box mt={4}>
|
||||||
|
<Divider />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
|
||||||
|
This report contains confidential security information. Distribution should be limited to authorized personnel only.
|
||||||
|
Report generated on {format(new Date(), 'PPP')} from data last updated {format(new Date(summary.lastUpdated), 'PPp')}.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/app/api/reporting/compliance-summary/route.ts
Normal file
20
src/app/api/reporting/compliance-summary/route.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Mock compliance summary data
|
||||||
|
// In production, this would query your database for actual metrics
|
||||||
|
const summary = {
|
||||||
|
totalDevices: 127,
|
||||||
|
vulnerableDevices: 89,
|
||||||
|
totalVulnerabilities: 342,
|
||||||
|
criticalVulns: 23,
|
||||||
|
highVulns: 67,
|
||||||
|
mediumVulns: 156,
|
||||||
|
lowVulns: 96,
|
||||||
|
totalSoftware: 1247,
|
||||||
|
vulnerableSoftware: 89,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(summary);
|
||||||
|
}
|
||||||
66
src/app/api/reporting/top-vulnerabilities/route.ts
Normal file
66
src/app/api/reporting/top-vulnerabilities/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Mock top vulnerabilities data
|
||||||
|
// In production, this would query your vulnerability database
|
||||||
|
const topVulns = [
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12345',
|
||||||
|
title: 'Critical Remote Code Execution in Apache HTTP Server',
|
||||||
|
severity: 'CRITICAL',
|
||||||
|
score: 9.8,
|
||||||
|
affectedDevices: 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12346',
|
||||||
|
title: 'Privilege Escalation Vulnerability in OpenSSL',
|
||||||
|
severity: 'HIGH',
|
||||||
|
score: 8.4,
|
||||||
|
affectedDevices: 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12347',
|
||||||
|
title: 'Information Disclosure in Microsoft Office',
|
||||||
|
severity: 'HIGH',
|
||||||
|
score: 7.9,
|
||||||
|
affectedDevices: 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12348',
|
||||||
|
title: 'Cross-Site Scripting in Adobe Reader',
|
||||||
|
severity: 'MEDIUM',
|
||||||
|
score: 6.1,
|
||||||
|
affectedDevices: 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12349',
|
||||||
|
title: 'Denial of Service in Java Runtime Environment',
|
||||||
|
severity: 'MEDIUM',
|
||||||
|
score: 5.8,
|
||||||
|
affectedDevices: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12350',
|
||||||
|
title: 'Buffer Overflow in Windows Kernel',
|
||||||
|
severity: 'HIGH',
|
||||||
|
score: 7.5,
|
||||||
|
affectedDevices: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12351',
|
||||||
|
title: 'SQL Injection in MySQL Client',
|
||||||
|
severity: 'MEDIUM',
|
||||||
|
score: 6.8,
|
||||||
|
affectedDevices: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cveId: 'CVE-2024-12352',
|
||||||
|
title: 'Use After Free in Chrome Browser',
|
||||||
|
severity: 'HIGH',
|
||||||
|
score: 8.1,
|
||||||
|
affectedDevices: 8
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return NextResponse.json(topVulns);
|
||||||
|
}
|
||||||
70
src/app/api/reporting/vulnerable-software/route.ts
Normal file
70
src/app/api/reporting/vulnerable-software/route.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Mock vulnerable software data
|
||||||
|
// In production, this would query your software inventory database
|
||||||
|
const vulnerableSoftware = [
|
||||||
|
{
|
||||||
|
softwareName: 'Apache HTTP Server',
|
||||||
|
totalInstances: 45,
|
||||||
|
vulnerableInstances: 23,
|
||||||
|
totalCves: 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'OpenSSL',
|
||||||
|
totalInstances: 89,
|
||||||
|
vulnerableInstances: 34,
|
||||||
|
totalCves: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Microsoft Office',
|
||||||
|
totalInstances: 127,
|
||||||
|
vulnerableInstances: 67,
|
||||||
|
totalCves: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Adobe Reader',
|
||||||
|
totalInstances: 98,
|
||||||
|
vulnerableInstances: 45,
|
||||||
|
totalCves: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Java Runtime Environment',
|
||||||
|
totalInstances: 76,
|
||||||
|
vulnerableInstances: 28,
|
||||||
|
totalCves: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Google Chrome',
|
||||||
|
totalInstances: 112,
|
||||||
|
vulnerableInstances: 23,
|
||||||
|
totalCves: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Mozilla Firefox',
|
||||||
|
totalInstances: 67,
|
||||||
|
vulnerableInstances: 18,
|
||||||
|
totalCves: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'MySQL Server',
|
||||||
|
totalInstances: 34,
|
||||||
|
vulnerableInstances: 12,
|
||||||
|
totalCves: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Python',
|
||||||
|
totalInstances: 89,
|
||||||
|
vulnerableInstances: 15,
|
||||||
|
totalCves: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
softwareName: 'Node.js',
|
||||||
|
totalInstances: 56,
|
||||||
|
vulnerableInstances: 9,
|
||||||
|
totalCves: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return NextResponse.json(vulnerableSoftware);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import DashboardIcon from '@mui/icons-material/Dashboard';
|
|||||||
import SecurityIcon from '@mui/icons-material/Security';
|
import SecurityIcon from '@mui/icons-material/Security';
|
||||||
import DevicesIcon from '@mui/icons-material/Devices';
|
import DevicesIcon from '@mui/icons-material/Devices';
|
||||||
import AppsIcon from '@mui/icons-material/Apps';
|
import AppsIcon from '@mui/icons-material/Apps';
|
||||||
|
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||||
@@ -123,6 +124,7 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
|
|||||||
{ label: 'Vulnerabilities', path: '/vulnerabilities', icon: <SecurityIcon /> },
|
{ label: 'Vulnerabilities', path: '/vulnerabilities', icon: <SecurityIcon /> },
|
||||||
{ label: 'Devices', path: '/devices', icon: <DevicesIcon /> },
|
{ label: 'Devices', path: '/devices', icon: <DevicesIcon /> },
|
||||||
{ label: 'Software', path: '/software', icon: <AppsIcon /> },
|
{ label: 'Software', path: '/software', icon: <AppsIcon /> },
|
||||||
|
{ label: 'Reporting', path: '/reporting', icon: <AssessmentIcon /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
// src/components/admin/UserTableSection.tsx
|
// src/components/admin/UserTableSection.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Typography, Table, TableBody, TableCell, TableContainer, TableHead,
|
Typography, Table, TableBody, TableCell, TableContainer, TableHead,
|
||||||
TableRow, Paper, Box, Button, Dialog, DialogTitle, DialogContent, DialogActions
|
TableRow, Paper, Box, Button, Dialog, DialogTitle, DialogContent, DialogActions,
|
||||||
|
TextField, Select, MenuItem, FormControl, InputLabel, IconButton
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import UserEnableToggle from './UserEnableToggle';
|
import UserEnableToggle from './UserEnableToggle';
|
||||||
import AddUserForm from './forms/AddUserForm';
|
import AddUserForm from './forms/AddUserForm';
|
||||||
import AddClientForm from './forms/AddClientForm';
|
import AddClientForm from './forms/AddClientForm';
|
||||||
import InviteUserForm from './forms/InviteUserForm';
|
import InviteUserForm from './forms/InviteUserForm';
|
||||||
|
import api from '@/lib/axios';
|
||||||
|
|
||||||
interface UserDTO {
|
interface UserDTO {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,15 +25,97 @@ interface UserDTO {
|
|||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
|
clientId?: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClientDTO {
|
||||||
|
clientId: number;
|
||||||
|
clientIdentifier: string;
|
||||||
|
clientName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserTableSection({ initialUsers }: { initialUsers: UserDTO[] }) {
|
export default function UserTableSection({ initialUsers }: { initialUsers: UserDTO[] }) {
|
||||||
const [users, setUsers] = useState<UserDTO[]>(initialUsers);
|
const [users, setUsers] = useState<UserDTO[]>(initialUsers);
|
||||||
const [openUserDialog, setOpenUserDialog] = useState(false);
|
const [openUserDialog, setOpenUserDialog] = useState(false);
|
||||||
const [openClientDialog, setOpenClientDialog] = useState(false);
|
const [openClientDialog, setOpenClientDialog] = useState(false);
|
||||||
const [openInviteUserDialog, setOpenInviteUserDialog] = useState(false);
|
const [openInviteUserDialog, setOpenInviteUserDialog] = useState(false);
|
||||||
|
|
||||||
|
// Edit mode state
|
||||||
|
const [editingUserId, setEditingUserId] = useState<number | null>(null);
|
||||||
|
const [editFormData, setEditFormData] = useState<Partial<UserDTO>>({});
|
||||||
|
const [clients, setClients] = useState<ClientDTO[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Fetch clients for dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchClients = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/auth/clients');
|
||||||
|
setClients(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch clients:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchClients();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEdit = (user: UserDTO) => {
|
||||||
|
setEditingUserId(user.id);
|
||||||
|
setEditFormData({
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
clientName: user.clientName,
|
||||||
|
clientId: user.clientId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditingUserId(null);
|
||||||
|
setEditFormData({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (userId: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Find the selected client
|
||||||
|
const selectedClient = clients.find(c => c.clientId === editFormData.clientId);
|
||||||
|
|
||||||
|
await api.put(`/admin/users/${userId}`, {
|
||||||
|
username: editFormData.username,
|
||||||
|
displayName: editFormData.displayName,
|
||||||
|
firstName: editFormData.firstName,
|
||||||
|
lastName: editFormData.lastName,
|
||||||
|
email: editFormData.email,
|
||||||
|
role: editFormData.role,
|
||||||
|
clientId: editFormData.clientId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
setUsers(users.map(user =>
|
||||||
|
user.id === userId
|
||||||
|
? {
|
||||||
|
...user,
|
||||||
|
...editFormData,
|
||||||
|
clientName: selectedClient?.clientName || user.clientName
|
||||||
|
}
|
||||||
|
: user
|
||||||
|
));
|
||||||
|
|
||||||
|
setEditingUserId(null);
|
||||||
|
setEditFormData({});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update user:', err);
|
||||||
|
alert('Failed to update user. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 4 }}>
|
<Box sx={{ p: 4 }}>
|
||||||
<Typography variant="h5" gutterBottom>User Management</Typography>
|
<Typography variant="h5" gutterBottom>User Management</Typography>
|
||||||
@@ -57,23 +144,143 @@ export default function UserTableSection({ initialUsers }: { initialUsers: UserD
|
|||||||
<TableCell>Role</TableCell>
|
<TableCell>Role</TableCell>
|
||||||
<TableCell>Client</TableCell>
|
<TableCell>Client</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell>Actions</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{users.map((user) => (
|
{users.map((user) => {
|
||||||
<TableRow key={user.id}>
|
const isEditing = editingUserId === user.id;
|
||||||
<TableCell>{user.username}</TableCell>
|
return (
|
||||||
<TableCell>{user.displayName}</TableCell>
|
<TableRow key={user.id}>
|
||||||
<TableCell>{user.firstName}</TableCell>
|
<TableCell>
|
||||||
<TableCell>{user.lastName}</TableCell>
|
{isEditing ? (
|
||||||
<TableCell>{user.email}</TableCell>
|
<TextField
|
||||||
<TableCell>{user.role}</TableCell>
|
size="small"
|
||||||
<TableCell>{user.clientName}</TableCell>
|
value={editFormData.username || ''}
|
||||||
<TableCell>
|
onChange={(e) => setEditFormData({ ...editFormData, username: e.target.value })}
|
||||||
<UserEnableToggle userId={user.id} initialEnabled={user.enabled} />
|
fullWidth
|
||||||
</TableCell>
|
/>
|
||||||
</TableRow>
|
) : (
|
||||||
))}
|
user.username
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={editFormData.displayName || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, displayName: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
user.displayName
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={editFormData.firstName || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
user.firstName
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={editFormData.lastName || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
user.lastName
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={editFormData.email || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
user.email
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<Select
|
||||||
|
value={editFormData.role || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
|
||||||
|
>
|
||||||
|
<MenuItem value="USER">USER</MenuItem>
|
||||||
|
<MenuItem value="ADMIN">ADMIN</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
user.role
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<Select
|
||||||
|
value={editFormData.clientId || ''}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, clientId: Number(e.target.value) })}
|
||||||
|
>
|
||||||
|
{clients.map(client => (
|
||||||
|
<MenuItem key={client.clientId} value={client.clientId}>
|
||||||
|
{client.clientName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
user.clientName
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<UserEnableToggle userId={user.id} initialEnabled={user.enabled} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleSave(user.id)}
|
||||||
|
disabled={loading}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<SaveIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color="default"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={loading}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<CancelIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleEdit(user)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
||||||
import api from '@/lib/axios';
|
import api from '@/lib/axios';
|
||||||
|
import type { AxiosError } from 'axios';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { DetailedDevice } from '@/types/devices';
|
import { DetailedDevice } from '@/types/devices';
|
||||||
import { disableConsoleInProd } from '@/lib/disableConsole';
|
import { disableConsoleInProd } from '@/lib/disableConsole';
|
||||||
disableConsoleInProd();
|
disableConsoleInProd();
|
||||||
|
|
||||||
// Helper function to convert UTC timestamps to local time
|
|
||||||
const convertUtcToLocal = (utcString: string | null): string | null => {
|
|
||||||
if (!utcString) return null;
|
|
||||||
// Add 'Z' to indicate it's UTC, then convert to local time
|
|
||||||
return new Date(utcString + 'Z').toLocaleString();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface DriveInfo {
|
interface DriveInfo {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,13 +24,698 @@ interface MacAddress {
|
|||||||
interfaceName: string;
|
interfaceName: string;
|
||||||
macAddress: string;
|
macAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InstalledApp {
|
interface InstalledApp {
|
||||||
app_name: string;
|
app_name: string;
|
||||||
app_version: string;
|
app_version: string;
|
||||||
publisher: string;
|
publisher: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
const normalizeKey = (key: string): string =>
|
||||||
|
key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
|
||||||
|
const getDirectValue = (source: RawRecord | null | undefined, keys: string[]): unknown => {
|
||||||
|
if (!source) return undefined;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||||
|
const value = source[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findValueDeep = (
|
||||||
|
source: unknown,
|
||||||
|
normalizedTargets: string[],
|
||||||
|
seen = new WeakSet<object>()
|
||||||
|
): unknown => {
|
||||||
|
if (!source || typeof source !== 'object') return undefined;
|
||||||
|
|
||||||
|
const stack: unknown[] = [source];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const current = stack.pop();
|
||||||
|
if (!current || typeof current !== 'object') continue;
|
||||||
|
|
||||||
|
if (seen.has(current as object)) continue;
|
||||||
|
seen.add(current as object);
|
||||||
|
|
||||||
|
if (Array.isArray(current)) {
|
||||||
|
for (const item of current) {
|
||||||
|
if (item && (typeof item === 'object' || typeof item === 'string')) {
|
||||||
|
stack.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(current as RawRecord)) {
|
||||||
|
const normalizedKey = normalizeKey(key);
|
||||||
|
if (normalizedTargets.includes(normalizedKey) && value !== undefined && value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
stack.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveValue = (
|
||||||
|
source: RawRecord | null | undefined,
|
||||||
|
keys: string[],
|
||||||
|
fallbackKeys: string[] = keys
|
||||||
|
): unknown => {
|
||||||
|
if (!source) return undefined;
|
||||||
|
|
||||||
|
const direct = getDirectValue(source, keys);
|
||||||
|
if (direct !== undefined && direct !== null) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTargets = Array.from(new Set(fallbackKeys.map(normalizeKey))).filter(Boolean);
|
||||||
|
return findValueDeep(source, normalizedTargets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveArray = <T = unknown>(
|
||||||
|
source: RawRecord | null | undefined,
|
||||||
|
keys: string[],
|
||||||
|
fallbackKeys?: string[]
|
||||||
|
): T[] | undefined => {
|
||||||
|
const value = resolveValue(source, keys, fallbackKeys ?? keys);
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value as T[];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toOptionalString = (value: unknown): string | undefined => {
|
||||||
|
if (value === null || value === undefined) return undefined;
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toNonEmptyString = (value: unknown, fallback = ''): string => {
|
||||||
|
const candidate = toOptionalString(value)?.trim();
|
||||||
|
return candidate && candidate.length > 0 ? candidate : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeName = (value: string, hostname: string): string => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
if (trimmed.toLowerCase() === hostname.toLowerCase()) return '';
|
||||||
|
if (/^[A-Z0-9_-]+$/.test(trimmed)) return '';
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeArchitecture = (value: string): string => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || trimmed === '-1') return '';
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSizeToGB = (value: unknown): number => {
|
||||||
|
if (value === null || value === undefined) return 0;
|
||||||
|
|
||||||
|
const normalizeNumeric = (num: number): number => {
|
||||||
|
if (!Number.isFinite(num)) return 0;
|
||||||
|
if (num > 1024 ** 3) return +(num / 1024 ** 3).toFixed(2); // bytes
|
||||||
|
if (num > 1024 ** 2) return +(num / 1024 ** 2).toFixed(2); // KB
|
||||||
|
if (num > 1024) return +(num / 1024).toFixed(2); // MB
|
||||||
|
return +num.toFixed(2); // already GB
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return normalizeNumeric(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return 0;
|
||||||
|
|
||||||
|
const numeric = parseFloat(trimmed.replace(/[^\d.\-]/g, ''));
|
||||||
|
if (!Number.isFinite(numeric)) return 0;
|
||||||
|
|
||||||
|
if (/tb/i.test(trimmed)) return +(numeric * 1024).toFixed(2);
|
||||||
|
if (/gb/i.test(trimmed)) return +numeric.toFixed(2);
|
||||||
|
if (/mb/i.test(trimmed)) return +(numeric / 1024).toFixed(2);
|
||||||
|
if (/kb/i.test(trimmed)) return +(numeric / (1024 ** 2)).toFixed(2);
|
||||||
|
if (/bytes?/i.test(trimmed)) return +(numeric / (1024 ** 3)).toFixed(2);
|
||||||
|
|
||||||
|
return normalizeNumeric(numeric);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDrive = (driveInput: unknown, index: number): DriveInfo | null => {
|
||||||
|
if (!driveInput || typeof driveInput !== 'object') return null;
|
||||||
|
const drive = driveInput as RawRecord;
|
||||||
|
|
||||||
|
const name = toNonEmptyString(
|
||||||
|
resolveValue(drive, ['name', 'driveName', 'label', 'volume'], [
|
||||||
|
'name',
|
||||||
|
'driveName',
|
||||||
|
'label',
|
||||||
|
'volume',
|
||||||
|
'drive_letter',
|
||||||
|
'driveletter',
|
||||||
|
'mount_point',
|
||||||
|
'mountpoint',
|
||||||
|
'logicaldisk',
|
||||||
|
'deviceid',
|
||||||
|
]) ?? `Drive ${index + 1}`,
|
||||||
|
`Drive ${index + 1}`
|
||||||
|
);
|
||||||
|
const driveType = toNonEmptyString(
|
||||||
|
resolveValue(drive, ['driveType', 'drive_type', 'type'], [
|
||||||
|
'driveType',
|
||||||
|
'drive_type',
|
||||||
|
'type',
|
||||||
|
'drivetype',
|
||||||
|
'mediatype',
|
||||||
|
'busType',
|
||||||
|
]),
|
||||||
|
'Unknown'
|
||||||
|
);
|
||||||
|
const totalSizeGB = parseSizeToGB(
|
||||||
|
resolveValue(
|
||||||
|
drive,
|
||||||
|
['totalSizeGB', 'total_size_gb', 'totalSize', 'total_size', 'size', 'capacity', 'totalBytes', 'total_bytes'],
|
||||||
|
[
|
||||||
|
'totalSizeGB',
|
||||||
|
'total_size_gb',
|
||||||
|
'totalSize',
|
||||||
|
'total_size',
|
||||||
|
'size',
|
||||||
|
'capacity',
|
||||||
|
'totalBytes',
|
||||||
|
'total_bytes',
|
||||||
|
'size_bytes',
|
||||||
|
'capacity_bytes',
|
||||||
|
'totalsize',
|
||||||
|
'totalspace',
|
||||||
|
'totalspacebytes',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const freeSpaceGB = parseSizeToGB(
|
||||||
|
resolveValue(
|
||||||
|
drive,
|
||||||
|
['freeSpaceGB', 'free_space_gb', 'freeSpace', 'free_space', 'free', 'freeBytes', 'free_bytes', 'availableBytes'],
|
||||||
|
[
|
||||||
|
'freeSpaceGB',
|
||||||
|
'free_space_gb',
|
||||||
|
'freeSpace',
|
||||||
|
'free_space',
|
||||||
|
'free',
|
||||||
|
'freeBytes',
|
||||||
|
'free_bytes',
|
||||||
|
'availableBytes',
|
||||||
|
'available_bytes',
|
||||||
|
'freespace',
|
||||||
|
'freespacebytes',
|
||||||
|
'remaining',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!name && !totalSizeGB && !freeSpaceGB) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
driveType,
|
||||||
|
totalSizeGB,
|
||||||
|
freeSpaceGB,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractDrives = (rawInput: unknown): DriveInfo[] => {
|
||||||
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||||
|
const raw = rawInput as RawRecord;
|
||||||
|
|
||||||
|
const potentialArrays = [
|
||||||
|
resolveArray(raw, ['drives']),
|
||||||
|
resolveArray(raw, ['driveInfo']),
|
||||||
|
resolveArray(raw, ['drive_info']),
|
||||||
|
resolveArray(raw, ['storageDevices']),
|
||||||
|
resolveArray(raw, ['drive_details']),
|
||||||
|
resolveArray(raw, ['storage']),
|
||||||
|
resolveArray(raw, ['logicalDrives']),
|
||||||
|
resolveArray(raw, ['logical_drives']),
|
||||||
|
resolveArray(raw, ['volumes']),
|
||||||
|
resolveArray(raw, ['volume_info']),
|
||||||
|
resolveArray(raw, ['disks']),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of potentialArrays) {
|
||||||
|
if (Array.isArray(candidate)) {
|
||||||
|
return candidate
|
||||||
|
.map((drive, idx: number) => normalizeDrive(drive, idx))
|
||||||
|
.filter((drive): drive is DriveInfo => Boolean(drive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggregatedDrive = {
|
||||||
|
totalSize: resolveValue(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage'], [
|
||||||
|
'total_disk_space',
|
||||||
|
'totalDiskSpace',
|
||||||
|
'total_storage',
|
||||||
|
'totalSpace',
|
||||||
|
'total_space',
|
||||||
|
'storage_total',
|
||||||
|
'disk_total',
|
||||||
|
]),
|
||||||
|
freeSpace: resolveValue(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage'], [
|
||||||
|
'free_disk_space',
|
||||||
|
'freeDiskSpace',
|
||||||
|
'available_storage',
|
||||||
|
'free_storage',
|
||||||
|
'freeSpace',
|
||||||
|
'free_space',
|
||||||
|
'availableSpace',
|
||||||
|
'available_space',
|
||||||
|
'storage_free',
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (aggregatedDrive.totalSize || aggregatedDrive.freeSpace) {
|
||||||
|
const normalized = normalizeDrive(
|
||||||
|
{
|
||||||
|
name: 'System',
|
||||||
|
driveType: 'Unknown',
|
||||||
|
totalSizeGB: aggregatedDrive.totalSize,
|
||||||
|
freeSpaceGB: aggregatedDrive.freeSpace,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return normalized ? [normalized] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeIpAddress = (entryInput: unknown, index: number): IpAddress | null => {
|
||||||
|
if (!entryInput || typeof entryInput !== 'object') return null;
|
||||||
|
const entry = entryInput as RawRecord;
|
||||||
|
|
||||||
|
const ipAddress = toNonEmptyString(
|
||||||
|
resolveValue(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6'], [
|
||||||
|
'ipAddress',
|
||||||
|
'ip_address',
|
||||||
|
'address',
|
||||||
|
'ip',
|
||||||
|
'ipv4',
|
||||||
|
'ipv6',
|
||||||
|
'primary_ip',
|
||||||
|
'primaryIp',
|
||||||
|
'lan_ip',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
const interfaceName = toNonEmptyString(
|
||||||
|
resolveValue(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic'], [
|
||||||
|
'interfaceName',
|
||||||
|
'interface_name',
|
||||||
|
'adapter',
|
||||||
|
'name',
|
||||||
|
'nic',
|
||||||
|
'network_adapter',
|
||||||
|
'interface',
|
||||||
|
'description',
|
||||||
|
]) ?? '',
|
||||||
|
ipAddress ? `Interface ${index + 1}` : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ipAddress && !interfaceName) return null;
|
||||||
|
|
||||||
|
const macAddress = toOptionalString(
|
||||||
|
resolveValue(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress'], [
|
||||||
|
'macAddress',
|
||||||
|
'mac_address',
|
||||||
|
'mac',
|
||||||
|
'physicalAddress',
|
||||||
|
'physical_address',
|
||||||
|
'hw_address',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
interfaceName: interfaceName || `Interface ${index + 1}`,
|
||||||
|
ipAddress: ipAddress || 'Unknown',
|
||||||
|
macAddress: macAddress?.trim() || undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractIpAddresses = (rawInput: unknown): IpAddress[] => {
|
||||||
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||||
|
const raw = rawInput as RawRecord;
|
||||||
|
|
||||||
|
const potentialArrays = [
|
||||||
|
resolveArray(raw, ['ipAddresses']),
|
||||||
|
resolveArray(raw, ['ip_addresses']),
|
||||||
|
resolveArray(raw, ['networkInterfaces']),
|
||||||
|
resolveArray(raw, ['network_interfaces']),
|
||||||
|
resolveArray(raw, ['interfaces']),
|
||||||
|
resolveArray(raw, ['networkAdapters']),
|
||||||
|
resolveArray(raw, ['network_adapters']),
|
||||||
|
resolveArray(raw, ['network']),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of potentialArrays) {
|
||||||
|
if (Array.isArray(candidate)) {
|
||||||
|
return candidate
|
||||||
|
.map((entry, idx: number) => normalizeIpAddress(entry, idx))
|
||||||
|
.filter((entry): entry is IpAddress => Boolean(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleIp = resolveValue(raw, ['ipAddress', 'ip_address', 'primary_ip', 'primaryIp']);
|
||||||
|
if (singleIp) {
|
||||||
|
return [
|
||||||
|
normalizeIpAddress(
|
||||||
|
{
|
||||||
|
ipAddress: singleIp,
|
||||||
|
interfaceName: resolveValue(raw, ['interfaceName', 'interface_name', 'adapter']),
|
||||||
|
macAddress: resolveValue(raw, ['macAddress', 'mac_address']),
|
||||||
|
},
|
||||||
|
0
|
||||||
|
) as IpAddress,
|
||||||
|
].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeInstalledApp = (appInput: unknown): InstalledApp | null => {
|
||||||
|
if (!appInput || typeof appInput !== 'object') return null;
|
||||||
|
const app = appInput as RawRecord;
|
||||||
|
|
||||||
|
const appName = toNonEmptyString(
|
||||||
|
resolveValue(app, ['app_name', 'name', 'title'], [
|
||||||
|
'app_name',
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'display_name',
|
||||||
|
'product_name',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
if (!appName) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
app_name: appName,
|
||||||
|
app_version: toNonEmptyString(
|
||||||
|
resolveValue(app, ['app_version', 'version'], ['app_version', 'version', 'display_version', 'product_version']) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
publisher: toNonEmptyString(
|
||||||
|
resolveValue(app, ['publisher', 'manufacturer', 'vendor'], [
|
||||||
|
'publisher',
|
||||||
|
'manufacturer',
|
||||||
|
'vendor',
|
||||||
|
'company',
|
||||||
|
'publisher_name',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractInstalledApps = (rawInput: unknown): InstalledApp[] => {
|
||||||
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||||
|
const raw = rawInput as RawRecord;
|
||||||
|
|
||||||
|
const potentialArrays = [
|
||||||
|
resolveArray(raw, ['installedApplications']),
|
||||||
|
resolveArray(raw, ['installed_applications']),
|
||||||
|
resolveArray(raw, ['software']),
|
||||||
|
resolveArray(raw, ['applications']),
|
||||||
|
resolveArray(raw, ['apps']),
|
||||||
|
resolveArray(raw, ['installedPrograms']),
|
||||||
|
resolveArray(raw, ['installed_programs']),
|
||||||
|
resolveArray(raw, ['programs']),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of potentialArrays) {
|
||||||
|
if (Array.isArray(candidate)) {
|
||||||
|
return candidate
|
||||||
|
.map((app) => normalizeInstalledApp(app))
|
||||||
|
.filter((app): app is InstalledApp => Boolean(app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractMacAddresses = (rawInput: unknown, ipAddresses: IpAddress[]): MacAddress[] => {
|
||||||
|
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||||
|
const raw = rawInput as RawRecord;
|
||||||
|
const potentialArrays = resolveArray(raw, ['macAddresses', 'mac_addresses', 'macs', 'mac_list']);
|
||||||
|
|
||||||
|
if (Array.isArray(potentialArrays)) {
|
||||||
|
return potentialArrays
|
||||||
|
.map((entry, idx: number) => {
|
||||||
|
if (!entry || typeof entry !== 'object') return null;
|
||||||
|
const record = entry as RawRecord;
|
||||||
|
const macAddress = toOptionalString(
|
||||||
|
resolveValue(record, ['macAddress', 'mac_address', 'mac'], [
|
||||||
|
'macAddress',
|
||||||
|
'mac_address',
|
||||||
|
'mac',
|
||||||
|
'physicalAddress',
|
||||||
|
'hw_address',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
if (!macAddress) return null;
|
||||||
|
return {
|
||||||
|
interfaceName: toNonEmptyString(
|
||||||
|
resolveValue(record, ['interfaceName', 'interface_name', 'adapter'], [
|
||||||
|
'interfaceName',
|
||||||
|
'interface_name',
|
||||||
|
'adapter',
|
||||||
|
'network_adapter',
|
||||||
|
'name',
|
||||||
|
]) ?? `Interface ${idx + 1}`,
|
||||||
|
`Interface ${idx + 1}`
|
||||||
|
),
|
||||||
|
macAddress: macAddress.trim(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry): entry is MacAddress => Boolean(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipAddresses
|
||||||
|
.filter((entry) => Boolean(entry.macAddress))
|
||||||
|
.map((entry) => ({
|
||||||
|
interfaceName: entry.interfaceName,
|
||||||
|
macAddress: entry.macAddress as string,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitGpuNames = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((item) => toNonEmptyString(item, ''))
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value
|
||||||
|
.split(/[,;\n]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDevice = (rawInput: unknown): DetailedDevice => {
|
||||||
|
const raw: RawRecord =
|
||||||
|
rawInput && typeof rawInput === 'object' ? (rawInput as RawRecord) : {};
|
||||||
|
|
||||||
|
const deviceId = Number(
|
||||||
|
resolveValue(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk'], [
|
||||||
|
'deviceId',
|
||||||
|
'device_id',
|
||||||
|
'id',
|
||||||
|
'deviceID',
|
||||||
|
'device_id_pk',
|
||||||
|
]) ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const drives = extractDrives(raw);
|
||||||
|
const ipAddresses = extractIpAddresses(raw);
|
||||||
|
const macAddresses = extractMacAddresses(raw, ipAddresses);
|
||||||
|
const clientEntry = resolveValue(raw, ['client'], ['client', 'clientInfo', 'customer']);
|
||||||
|
const clientNameFromNested =
|
||||||
|
clientEntry && typeof clientEntry === 'object'
|
||||||
|
? resolveValue(clientEntry as RawRecord, ['name', 'clientName', 'client_name'])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const hostname = toNonEmptyString(
|
||||||
|
resolveValue(raw, ['hostname', 'hostName', 'computerName', 'deviceName'], [
|
||||||
|
'hostname',
|
||||||
|
'hostName',
|
||||||
|
'computerName',
|
||||||
|
'deviceName',
|
||||||
|
'host',
|
||||||
|
'machineName',
|
||||||
|
]) ?? `Device ${deviceId}`,
|
||||||
|
deviceId ? `Device ${deviceId}` : 'Unknown Device'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceId,
|
||||||
|
hostname,
|
||||||
|
osName: sanitizeName(
|
||||||
|
toNonEmptyString(
|
||||||
|
resolveValue(
|
||||||
|
raw,
|
||||||
|
['osName', 'os_name', 'operatingSystem', 'os'],
|
||||||
|
[
|
||||||
|
'osName',
|
||||||
|
'os_name',
|
||||||
|
'operatingSystem',
|
||||||
|
'operating_system',
|
||||||
|
'operatingSystemName',
|
||||||
|
'operating_system_name',
|
||||||
|
'osFriendlyName',
|
||||||
|
'os_caption',
|
||||||
|
'oscaption',
|
||||||
|
]
|
||||||
|
) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
hostname
|
||||||
|
),
|
||||||
|
osVersion: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['osVersion', 'os_version', 'osVersionString'], [
|
||||||
|
'osVersion',
|
||||||
|
'os_version',
|
||||||
|
'osVersionString',
|
||||||
|
'operatingSystemVersion',
|
||||||
|
'osversion',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
windowsVersion: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['windowsVersion', 'windows_version', 'osRelease'], [
|
||||||
|
'windowsVersion',
|
||||||
|
'windows_version',
|
||||||
|
'osRelease',
|
||||||
|
'windowsRelease',
|
||||||
|
'windows_release',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
windowsBuild: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['windowsBuild', 'windows_build', 'buildNumber'], [
|
||||||
|
'windowsBuild',
|
||||||
|
'windows_build',
|
||||||
|
'buildNumber',
|
||||||
|
'build_number',
|
||||||
|
'windowsBuildNumber',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
osArchitecture: sanitizeArchitecture(
|
||||||
|
toNonEmptyString(
|
||||||
|
resolveValue(raw, ['osArchitecture', 'os_architecture', 'architecture'], [
|
||||||
|
'osArchitecture',
|
||||||
|
'os_architecture',
|
||||||
|
'architecture',
|
||||||
|
'platform',
|
||||||
|
'osarch',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
processorName: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['processorName', 'processor_name', 'cpuName'], [
|
||||||
|
'processorName',
|
||||||
|
'processor_name',
|
||||||
|
'cpuName',
|
||||||
|
'cpu_name',
|
||||||
|
'processor',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
processorArchitecture: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['processorArchitecture', 'processor_architecture', 'cpuArchitecture'], [
|
||||||
|
'processorArchitecture',
|
||||||
|
'processor_architecture',
|
||||||
|
'cpuArchitecture',
|
||||||
|
'cpu_architecture',
|
||||||
|
'cpuArch',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
gpuNames: splitGpuNames(
|
||||||
|
resolveValue(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus', 'graphics']) ?? []
|
||||||
|
),
|
||||||
|
totalMemory: toNonEmptyString(
|
||||||
|
resolveValue(
|
||||||
|
raw,
|
||||||
|
['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB'],
|
||||||
|
[
|
||||||
|
'totalMemory',
|
||||||
|
'total_memory',
|
||||||
|
'memory',
|
||||||
|
'totalMemoryMb',
|
||||||
|
'totalMemoryMB',
|
||||||
|
'memory_total',
|
||||||
|
'ram_total',
|
||||||
|
'physicalMemory',
|
||||||
|
]
|
||||||
|
) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
lastBootTime: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime'], [
|
||||||
|
'lastBootTime',
|
||||||
|
'last_boot_time',
|
||||||
|
'bootTime',
|
||||||
|
'lastBoot',
|
||||||
|
'lastBootUpTime',
|
||||||
|
'last_boot',
|
||||||
|
'boot_time',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
lastCheckedIn: toNonEmptyString(
|
||||||
|
resolveValue(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at'], [
|
||||||
|
'lastCheckedIn',
|
||||||
|
'last_checked_in',
|
||||||
|
'lastSeen',
|
||||||
|
'checked_in_at',
|
||||||
|
'lastSeenAt',
|
||||||
|
'last_seen',
|
||||||
|
]) ?? '',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
drives,
|
||||||
|
ipAddresses,
|
||||||
|
macAddresses,
|
||||||
|
installedApplications: extractInstalledApps(raw),
|
||||||
|
clientName:
|
||||||
|
toOptionalString(
|
||||||
|
resolveValue(raw, ['clientName', 'client_name'], ['clientName', 'client_name', 'client']) ??
|
||||||
|
clientNameFromNested ??
|
||||||
|
resolveValue(raw, ['clientIdentifier', 'client_identifier'])
|
||||||
|
) ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
interface DeviceVulnerability {
|
interface DeviceVulnerability {
|
||||||
cveId: string;
|
cveId: string;
|
||||||
@@ -49,16 +726,21 @@ interface DeviceVulnerability {
|
|||||||
lastModifiedDate: string;
|
lastModifiedDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CachedSoftwareEntry {
|
interface CachedSoftwareEntry {
|
||||||
id: number;
|
id: number;
|
||||||
softwareName: string;
|
softwareName: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
version: string;
|
version: string;
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
totalCves: number;
|
totalCves: number;
|
||||||
lastUpdated: string;
|
lastUpdated: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RawDevicesResponse {
|
||||||
|
devices?: unknown[];
|
||||||
|
vulnerabilitiesByDevice?: { [deviceId: string]: DeviceVulnerability[] };
|
||||||
|
}
|
||||||
|
|
||||||
interface DeviceContextType {
|
interface DeviceContextType {
|
||||||
devices: DetailedDevice[];
|
devices: DetailedDevice[];
|
||||||
deviceVulns: { [deviceId: string]: DeviceVulnerability[] };
|
deviceVulns: { [deviceId: string]: DeviceVulnerability[] };
|
||||||
@@ -93,10 +775,12 @@ export const DeviceProvider = ({
|
|||||||
vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] };
|
vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] };
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const [devices, setDevices] = useState<DetailedDevice[]>(() => {
|
const [devices, setDevices] = useState<DetailedDevice[]>(() => {
|
||||||
if (initialData?.devices?.length) return initialData.devices;
|
if (initialData?.devices?.length) {
|
||||||
return [];
|
return initialData.devices.map((device) => normalizeDevice(device));
|
||||||
});
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const [deviceVulns, setDeviceVulns] = useState<{ [deviceId: string]: DeviceVulnerability[] }>(
|
const [deviceVulns, setDeviceVulns] = useState<{ [deviceId: string]: DeviceVulnerability[] }>(
|
||||||
initialData?.vulnerabilitiesByDevice ?? {}
|
initialData?.vulnerabilitiesByDevice ?? {}
|
||||||
@@ -104,11 +788,12 @@ export const DeviceProvider = ({
|
|||||||
|
|
||||||
const [detailedCveLookup, setDetailedCveLookup] = useState<{ [cveId: string]: DeviceVulnerability }>({});
|
const [detailedCveLookup, setDetailedCveLookup] = useState<{ [cveId: string]: DeviceVulnerability }>({});
|
||||||
|
|
||||||
const [cachedSoftware, setCachedSoftware] = useState<CachedSoftwareEntry[]>([]); // ⬅️ NEW
|
const [cachedSoftware, setCachedSoftware] = useState<CachedSoftwareEntry[]>([]); // ⬅️ NEW
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(!initialData);
|
const [loading, setLoading] = useState<boolean>(!initialData);
|
||||||
|
|
||||||
const { username, roles, loading: authLoading } = useAuth();
|
const { username, roles, loading: authLoading } = useAuth();
|
||||||
|
const rawDevicesRef = useRef<unknown[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (devices.length > 0 || initialData || authLoading || !username) return;
|
if (devices.length > 0 || initialData || authLoading || !username) return;
|
||||||
@@ -123,28 +808,34 @@ export const DeviceProvider = ({
|
|||||||
|
|
||||||
console.group('📡 Fetching Devices, Vulnerabilities, and Software');
|
console.group('📡 Fetching Devices, Vulnerabilities, and Software');
|
||||||
|
|
||||||
const [devicesRes, softwareRes] = await Promise.all([
|
const [devicesRes, softwareRes] = await Promise.all([
|
||||||
api.get(devicesEndpoint, { withCredentials: true }),
|
api.get<RawDevicesResponse>(devicesEndpoint, { withCredentials: true }),
|
||||||
api.get(softwareEndpoint, { withCredentials: true }),
|
api.get<CachedSoftwareEntry[]>(softwareEndpoint, { withCredentials: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('✅ Devices fetched:', devicesRes.data);
|
console.log('✅ Devices fetched:', devicesRes.data);
|
||||||
console.log('✅ Software fetched:', softwareRes.data);
|
console.log('✅ Software fetched:', softwareRes.data);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
// STEP 1: Set basic data (keep original UTC timestamps for calculations)
|
// STEP 1: Set basic data (keep original UTC timestamps for calculations)
|
||||||
setDevices(devicesRes.data.devices);
|
const rawDevices = Array.isArray(devicesRes.data?.devices) ? (devicesRes.data.devices as unknown[]) : [];
|
||||||
setDeviceVulns(devicesRes.data.vulnerabilitiesByDevice);
|
rawDevicesRef.current = rawDevices;
|
||||||
setCachedSoftware(softwareRes.data);
|
const normalizedDevices = rawDevices.length
|
||||||
|
? rawDevices.map((device) => normalizeDevice(device))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
setDevices(normalizedDevices);
|
||||||
|
setDeviceVulns(devicesRes.data?.vulnerabilitiesByDevice ?? {});
|
||||||
|
setCachedSoftware(softwareRes.data ?? []);
|
||||||
|
|
||||||
// STEP 2: Extract all unique CVE IDs
|
// STEP 2: Extract all unique CVE IDs
|
||||||
const uniqueCveIds = new Set<string>();
|
const uniqueCveIds = new Set<string>();
|
||||||
(Object.values(devicesRes.data.vulnerabilitiesByDevice) as DeviceVulnerability[][]).forEach((vulns) => {
|
Object.values(devicesRes.data?.vulnerabilitiesByDevice ?? {}).forEach((vulns) => {
|
||||||
vulns.forEach((vuln) => {
|
vulns.forEach((vuln) => {
|
||||||
if (vuln.cveId) {
|
if (vuln.cveId) {
|
||||||
uniqueCveIds.add(vuln.cveId);
|
uniqueCveIds.add(vuln.cveId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// STEP 3: Fetch detailed CVE data
|
// STEP 3: Fetch detailed CVE data
|
||||||
@@ -171,35 +862,41 @@ const cveDetailsRes = await api.get<DeviceVulnerability[]>('/vuln/cves/lookup',
|
|||||||
setDetailedCveLookup(lookupMap);
|
setDetailedCveLookup(lookupMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.group('❌ Device fetch failed');
|
console.group('❌ Device fetch failed');
|
||||||
console.error('🔴 Axios Error Message:', error.message);
|
const axiosError = error as AxiosError;
|
||||||
console.error('🧾 Axios Error Config:', error.config);
|
console.error('🔴 Axios Error Message:', axiosError.message);
|
||||||
if (error.response) {
|
console.error('🧾 Axios Error Config:', axiosError.config);
|
||||||
console.error('📉 Status:', error.response.status);
|
if (axiosError.response) {
|
||||||
console.error('📄 Response Headers:', error.response.headers);
|
console.error('📉 Status:', axiosError.response.status);
|
||||||
console.error('📄 Response Data:', error.response.data);
|
console.error('📄 Response Headers:', axiosError.response.headers);
|
||||||
} else if (error.request) {
|
console.error('📄 Response Data:', axiosError.response.data);
|
||||||
console.error('🛑 No response received:', error.request);
|
} else if (axiosError.request) {
|
||||||
}
|
console.error('🛑 No response received:', axiosError.request);
|
||||||
console.groupEnd();
|
} else {
|
||||||
} finally {
|
console.error('❓ Unexpected error:', error);
|
||||||
setLoading(false);
|
}
|
||||||
}
|
console.groupEnd();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fetchDevicesVulnsAndSoftware();
|
fetchDevicesVulnsAndSoftware();
|
||||||
}, [devices.length, initialData, username, authLoading]);
|
}, [devices.length, initialData, username, authLoading, roles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("🧠 Devices updated:", devices);
|
console.log('🧠 Devices updated:', devices);
|
||||||
}, [devices]);
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as Window & {
|
||||||
if (typeof window !== 'undefined') {
|
__debug_devices?: DetailedDevice[];
|
||||||
(window as any).__debug_devices = devices;
|
__debug_rawDevices?: unknown[];
|
||||||
}
|
}).__debug_devices = devices;
|
||||||
|
(window as Window & { __debug_rawDevices?: unknown[] }).__debug_rawDevices = rawDevicesRef.current;
|
||||||
|
}
|
||||||
|
}, [devices]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeviceContext.Provider value={{
|
<DeviceContext.Provider value={{
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ export interface DriveInfo {
|
|||||||
freeSpaceGB: number;
|
freeSpaceGB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IpAddress {
|
export interface IpAddress {
|
||||||
interfaceName: string;
|
interfaceName: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}
|
macAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MacAddress {
|
export interface MacAddress {
|
||||||
interfaceName: string;
|
interfaceName: string;
|
||||||
@@ -42,4 +43,4 @@ export interface DriveInfo {
|
|||||||
installedApplications: InstalledApp[];
|
installedApplications: InstalledApp[];
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user