diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json
index 5a2c8c6..dd5adc1 100644
--- a/Frontend/package-lock.json
+++ b/Frontend/package-lock.json
@@ -21,6 +21,7 @@
"gsap": "^3.14.2",
"lucide-react": "^1.7.0",
"motion": "^12.38.0",
+ "qrcode": "^1.5.4",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
@@ -94,7 +95,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -735,7 +735,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -947,6 +946,31 @@
"@noble/ciphers": "^1.0.0"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -954,6 +978,7 @@
"dev": true,
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1031,7 +1056,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1397,6 +1421,7 @@
"resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.4.tgz",
"integrity": "sha512-DGd9yeSQzflOWO3Y5mt1GRXkXH9O/yIMgbxPjwLI3jwu/3nAjoXXD26lEeFb6tclYlg0JAqTIs5d930G/qxHeA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@hey-api/types": "0.1.4",
"ansi-colors": "4.1.3",
@@ -1415,6 +1440,7 @@
"resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.3.1.tgz",
"integrity": "sha512-7atnpUkT8TyUPHYPLk91j/GyaqMuwTEHanLOe50Dlx0EEvNuQqFD52Yjg8x4KU0UFL1mWlyhE+sUE/wAtQ1N2A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jsdevtools/ono": "7.1.3",
"@types/json-schema": "7.0.15",
@@ -1462,6 +1488,7 @@
"resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.3.0.tgz",
"integrity": "sha512-G+4GPojdLEh9bUwRG88teMPM1HdqMm/IsJ38cbnNxhyDu1FkFGwilkA1EqnULCzfTam/ZoZkaLdmAd8xEh4Xsw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@hey-api/codegen-core": "0.7.4",
"@hey-api/json-schema-ref-parser": "1.3.1",
@@ -1484,6 +1511,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
+ "peer": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -1496,6 +1524,7 @@
"resolved": "https://registry.npmjs.org/@hey-api/spec-types/-/spec-types-0.1.0.tgz",
"integrity": "sha512-StS4RrAO5pyJCBwe6uF9MAuPflkztriW+FPnVb7oEjzDYv1sxPwP+f7fL6u6D+UVrKpZ/9bPNx/xXVdkeWPU6A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@hey-api/types": "0.1.4"
},
@@ -1507,7 +1536,8 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.4.tgz",
"integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@hono/node-server": {
"version": "1.19.12",
@@ -1712,7 +1742,8 @@
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
@@ -2244,7 +2275,6 @@
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"node-fetch": "^2.7.0"
}
@@ -2476,7 +2506,6 @@
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -3414,7 +3443,6 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/webxr": "*",
@@ -3773,7 +3801,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -3840,6 +3867,169 @@
"qrcode": "1.5.3"
}
},
+ "node_modules/@reown/appkit-ui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/@reown/appkit-ui/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/qrcode": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
+ "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "encode-utf8": "^1.0.3",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/@reown/appkit-ui/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@reown/appkit-ui/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@reown/appkit-utils": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz",
@@ -4129,7 +4319,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -4437,7 +4626,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -5161,7 +5349,6 @@
"resolved": "https://registry.npmjs.org/@solana/kit/-/kit-5.5.1.tgz",
"integrity": "sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@solana/accounts": "5.5.1",
"@solana/addresses": "5.5.1",
@@ -5783,7 +5970,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz",
"integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/query-core": "5.81.5"
},
@@ -5957,7 +6143,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -5968,7 +6153,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -6000,7 +6184,6 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@@ -6204,7 +6387,6 @@
"resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz",
"integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"eventemitter3": "5.0.1",
"mipd": "0.0.7",
@@ -6589,7 +6771,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -7094,7 +7275,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7186,6 +7366,7 @@
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6"
}
@@ -7368,7 +7549,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -7604,7 +7784,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -7658,7 +7837,6 @@
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@@ -7696,6 +7874,7 @@
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz",
"integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"chokidar": "^5.0.0",
"confbox": "^0.2.2",
@@ -7724,6 +7903,7 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
@@ -7739,6 +7919,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
+ "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -7748,6 +7929,7 @@
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">= 20.19.0"
},
@@ -7923,7 +8105,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -7974,6 +8155,7 @@
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"consola": "^3.2.3"
}
@@ -8144,6 +8326,7 @@
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
+ "peer": true,
"bin": {
"color-support": "bin.js"
}
@@ -8180,13 +8363,15 @@
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
@@ -8718,7 +8903,6 @@
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz",
"integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@ecies/ciphers": "^0.2.5",
"@noble/ciphers": "^1.3.0",
@@ -8928,7 +9112,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9307,8 +9490,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/eventemitter3": {
"version": "5.0.1",
@@ -9452,7 +9634,8 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/extension-port-stream": {
"version": "3.0.0",
@@ -10053,6 +10236,7 @@
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
@@ -10065,6 +10249,7 @@
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
@@ -10266,7 +10451,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz",
"integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -10815,7 +10999,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -11893,6 +12076,7 @@
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
"integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"citty": "^0.2.0",
"pathe": "^2.0.3",
@@ -11909,7 +12093,8 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/obj-multiplex": {
"version": "1.0.0",
@@ -12016,7 +12201,8 @@
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/on-exit-leak-free": {
"version": "0.2.0",
@@ -12330,13 +12516,15 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/perfect-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/picocolors": {
"version": "1.1.1",
@@ -12430,6 +12618,7 @@
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
@@ -12580,7 +12769,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -12614,7 +12802,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -12923,13 +13110,12 @@
}
},
"node_modules/qrcode": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
- "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
- "encode-utf8": "^1.0.3",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
@@ -13183,6 +13369,7 @@
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
@@ -13193,7 +13380,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13213,7 +13399,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -13364,7 +13549,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -13484,6 +13668,7 @@
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
@@ -13975,7 +14160,6 @@
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
@@ -14531,6 +14715,57 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/thirdweb/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/thirdweb/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/thirdweb/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/thirdweb/node_modules/open": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
@@ -14549,6 +14784,118 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/thirdweb/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/thirdweb/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/qrcode": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz",
+ "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "encode-utf8": "^1.0.3",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/thirdweb/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/thirdweb/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thirdweb/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/thread-stream": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz",
@@ -14562,8 +14909,7 @@
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@@ -14609,6 +14955,7 @@
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
"integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -15198,7 +15545,6 @@
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@@ -15258,7 +15604,6 @@
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz",
"integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"derive-valtio": "0.1.0",
"proxy-compare": "2.6.0",
@@ -15310,7 +15655,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
@@ -15423,7 +15767,6 @@
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -15501,7 +15844,6 @@
"resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz",
"integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@wagmi/connectors": "6.2.0",
"@wagmi/core": "2.22.1",
@@ -15683,7 +16025,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -15983,7 +16324,6 @@
"resolved": "https://registry.npmjs.org/@solana/kit/-/kit-2.3.0.tgz",
"integrity": "sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@solana/accounts": "2.3.0",
"@solana/addresses": "2.3.0",
@@ -16340,7 +16680,6 @@
"resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-2.3.0.tgz",
"integrity": "sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@solana/accounts": "2.3.0",
"@solana/codecs": "2.3.0",
@@ -16591,7 +16930,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.75.tgz",
"integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/Frontend/package.json b/Frontend/package.json
index bf0e78f..31cb0d7 100644
--- a/Frontend/package.json
+++ b/Frontend/package.json
@@ -23,6 +23,7 @@
"gsap": "^3.14.2",
"lucide-react": "^1.7.0",
"motion": "^12.38.0",
+ "qrcode": "^1.5.4",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
diff --git a/Frontend/src/components/dashboard/DoctorDashboard.jsx b/Frontend/src/components/dashboard/DoctorDashboard.jsx
index 3df6423..cc559d1 100644
--- a/Frontend/src/components/dashboard/DoctorDashboard.jsx
+++ b/Frontend/src/components/dashboard/DoctorDashboard.jsx
@@ -5,7 +5,7 @@ import { upload } from "thirdweb/storage";
const client = createThirdwebClient({ clientId: import.meta.env.VITE_CLIENT_ID });
-import { LayoutDashboard, Users, Clock, Settings, LogOut, Activity, Search, Plus, Calendar, Menu, HelpCircle, Mail, ShieldCheck, Upload, FileSignature, FileUp, ShieldAlert, ChevronsLeft, ClipboardList, Bell, Download, Loader2, X, Eye, Edit } from 'lucide-react';
+import { LayoutDashboard, Users, Clock, Settings, LogOut, Activity, Search, Plus, Calendar, Menu, HelpCircle, Mail, ShieldCheck, Upload, FileSignature, FileUp, ShieldAlert, ChevronsLeft, ClipboardList, Bell, Download, Loader2, X, Eye, Edit, QrCode } from 'lucide-react';
import { AnimatedThemeToggler } from '../magicui/animated-theme-toggler';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@@ -15,7 +15,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { AmbientParticles } from '../effects/AmbientParticles';
import { GlassCard } from '../effects/GlassCard';
import { useAuth } from '../../context/AuthContext';
-import { getWaitingRoom, getAccessibleRecords, completeAppointment, mintRecord, amendRecord } from '../../services/api';
+import { getWaitingRoom, getAccessibleRecords, completeAppointment, mintRecord, amendRecord, createEpisode } from '../../services/api';
const NAV = {
main: [{ id: 'overview', label: 'Overview', icon: LayoutDashboard }, { id: 'waiting', label: 'Waiting Room', icon: Clock }],
@@ -75,6 +75,7 @@ export default function DoctorDashboard() {
const [selectedPatient, setSelectedPatient] = useState(null);
const [manualSearchQuery, setManualSearchQuery] = useState('');
+ const [qrPayloadInput, setQrPayloadInput] = useState('');
const [fileToMint, setFileToMint] = useState(null);
const [patientAddressToMint, setPatientAddressToMint] = useState('');
@@ -84,6 +85,14 @@ export default function DoctorDashboard() {
const [amendFile, setAmendFile] = useState(null);
const [isAmending, setIsAmending] = useState(false);
+ // Episode state
+ const [episodes, setEpisodes] = useState([]);
+ const [showEpisodeModal, setShowEpisodeModal] = useState(false);
+ const [episodeTitle, setEpisodeTitle] = useState('');
+ const [episodeDescription, setEpisodeDescription] = useState('');
+ const [isCreatingEpisode, setIsCreatingEpisode] = useState(false);
+ const [selectedEpisodeId, setSelectedEpisodeId] = useState('');
+
const displayName = user?.name || 'Doctor';
const normalizeCid = (value) => {
@@ -167,10 +176,13 @@ export default function DoctorDashboard() {
try {
console.log("Uploading to IPFS via Thirdweb...");
const uri = await upload({ client, files: [fileToMint] });
- await mintRecord(targetAddress, uri, fileToMint.name);
+ const epId = selectedEpisodeId ? parseInt(selectedEpisodeId, 10) : null;
+ if (selectedEpisodeId && isNaN(epId)) { setErrorMsg("Invalid episode selection."); return; }
+ await mintRecord(targetAddress, uri, fileToMint.name, null, epId);
setFileToMint(null);
setPatientAddressToMint('');
+ setSelectedEpisodeId('');
if (targetAddress === selectedPatient) {
await handleSelectPatient(targetAddress);
@@ -205,6 +217,41 @@ export default function DoctorDashboard() {
}
};
+ const handleCreateEpisode = async () => {
+ const targetAddress = selectedPatient || patientAddressToMint;
+ if (!episodeTitle.trim() || !targetAddress) { setErrorMsg("Episode title and patient address are required."); return; }
+ setIsCreatingEpisode(true); setErrorMsg('');
+ try {
+ const res = await createEpisode(targetAddress, episodeTitle.trim(), episodeDescription.trim());
+ const ep = res.data;
+ setEpisodes(prev => [ep, ...prev]);
+ setSelectedEpisodeId(String(ep.episodeId));
+ setEpisodeTitle('');
+ setEpisodeDescription('');
+ setShowEpisodeModal(false);
+ } catch (err) {
+ setErrorMsg(err.message);
+ } finally {
+ setIsCreatingEpisode(false);
+ }
+ };
+
+ const handleScanQrPayload = async () => {
+ if (!qrPayloadInput.trim()) return;
+ try {
+ const parsed = JSON.parse(qrPayloadInput);
+ if (parsed?.type !== 'MEDICHAIN_PATIENT') {
+ throw new Error('Unsupported QR payload type.');
+ }
+ const patientFromQr = parsed?.patientAddress || parsed?.patientWallet || '';
+ if (!patientFromQr) throw new Error('QR payload missing patientAddress.');
+ await handleSelectPatient(patientFromQr);
+ setQrPayloadInput('');
+ } catch (err) {
+ setErrorMsg('Invalid QR payload. Paste a valid MediChain QR payload JSON.');
+ }
+ };
+
const validRecords = grantedRecords.filter((r) => {
const recId = getRecordId(r);
if (recId === undefined || recId === null) return false;
@@ -265,11 +312,21 @@ export default function DoctorDashboard() {
Patient Vault
+ {selectedPatient && (
+
+ )}
setManualSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSelectPatient(manualSearchQuery)} className="border rounded-lg px-2 py-1 text-[11px] font-mono bg-background w-[140px]" />
{selectedPatient &&
}
+
+
+
+ setQrPayloadInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleScanQrPayload()} className="w-full border rounded-lg pl-8 pr-2 py-2 text-[10px] font-mono bg-background" />
+
+
+
{!selectedPatient ? (
Select or search a patient to view files.
@@ -374,6 +431,14 @@ export default function DoctorDashboard() {
setPatientAddressToMint(e.target.value)} className="w-full text-[11px] px-3 py-2.5 border rounded-xl bg-background font-mono" />
)}
setFileToMint(e.target.files?.[0] || null)} className="w-full text-[11px] file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:bg-muted file:text-foreground cursor-pointer" />
+ {episodes.length > 0 && (
+
+ )}
{isUploading ? : }{isUploading ? 'Minting...' : 'Mint Record'}
@@ -385,6 +450,35 @@ export default function DoctorDashboard() {
+
+ {/* New Episode Modal */}
+
+ {showEpisodeModal && (
+
+
+
+
New Episode of Care
+
+
+
+
+
+ setEpisodeTitle(e.target.value)} disabled={isCreatingEpisode} placeholder="e.g. Post-Op Cardiac Recovery" className="w-full px-3 py-2 rounded-xl border border-border bg-background/50 text-sm focus:outline-none focus:ring-2 focus:ring-secondary/40 disabled:opacity-50" />
+
+
+
+
+ {selectedPatient &&
Patient: {selectedPatient.slice(0, 14)}...
}
+
+
+
+
+ )}
+
);
}
diff --git a/Frontend/src/components/dashboard/InsurerDashboard.jsx b/Frontend/src/components/dashboard/InsurerDashboard.jsx
index 761ecc8..2ff9c81 100644
--- a/Frontend/src/components/dashboard/InsurerDashboard.jsx
+++ b/Frontend/src/components/dashboard/InsurerDashboard.jsx
@@ -15,7 +15,7 @@ import { motion } from 'framer-motion';
import { AmbientParticles } from '../effects/AmbientParticles';
import { GlassCard } from '../effects/GlassCard';
import { useAuth } from '../../context/AuthContext';
-import { viewRecordAsInsurer } from '../../services/api';
+import { verifyEpisodeAsInsurer, viewRecordAsInsurer } from '../../services/api';
const NAV = {
main: [{ id: 'overview', label: 'Overview', icon: LayoutDashboard }, { id: 'verify', label: 'Verify Claims', icon: FileSearch }],
@@ -88,6 +88,58 @@ function VerificationCheckRow({ icon: Icon, label, description, verified, delay
);
}
+function TrustScore({ providerVerified, integrityValid, isLatestVersion }) {
+ const SCORE_PROVIDER = 40;
+ const SCORE_INTEGRITY = 40;
+ const SCORE_LATEST = 20;
+ const SCORE_SUPERSEDED = 10;
+
+ const score = (providerVerified ? SCORE_PROVIDER : 0)
+ + (integrityValid ? SCORE_INTEGRITY : 0)
+ + (isLatestVersion ? SCORE_LATEST : SCORE_SUPERSEDED);
+ const tier = score >= 90 ? 'HIGH' : score >= 60 ? 'MEDIUM' : 'LOW';
+ const colors = {
+ HIGH: { bg: 'bg-emerald-900/20', border: 'border-emerald-500/40', text: 'text-emerald-400', dot: 'bg-emerald-500' },
+ MEDIUM: { bg: 'bg-yellow-900/20', border: 'border-yellow-500/40', text: 'text-yellow-400', dot: 'bg-yellow-500' },
+ LOW: { bg: 'bg-red-900/20', border: 'border-red-500/40', text: 'text-red-400', dot: 'bg-red-500' },
+ };
+ const c = colors[tier];
+ const reasons = [];
+ if (!providerVerified) reasons.push('Provider signature could not be verified');
+ if (!integrityValid) reasons.push('Cryptographic integrity check failed');
+ if (!isLatestVersion) reasons.push('Record has been superseded by a newer version');
+
+ return (
+
+
+
+ {score}
+ / 100
+
+
+ {reasons.length > 0 && (
+
+ {reasons.map((r, i) => (
+ -
+ {r}
+
+ ))}
+
+ )}
+
+ );
+}
+
+
export default function InsuranceDashboard() {
const account = useActiveAccount();
const { user, logout } = useAuth();
@@ -98,8 +150,11 @@ export default function InsuranceDashboard() {
const [mobileOpen, setMobileOpen] = useState(false);
const [walletAddress, setWalletAddress] = useState('');
const [tokenId, setTokenId] = useState('');
+ const [episodeId, setEpisodeId] = useState('');
const [isVerifying, setIsVerifying] = useState(false);
+ const [isVerifyingEpisode, setIsVerifyingEpisode] = useState(false);
const [verificationResult, setVerificationResult] = useState(null);
+ const [episodeVerification, setEpisodeVerification] = useState(null);
const [verifyError, setVerifyError] = useState('');
const [auditTrail, setAuditTrail] = useState([]);
@@ -121,6 +176,7 @@ export default function InsuranceDashboard() {
setIsVerifying(true);
setVerifyError('');
setVerificationResult(null);
+ setEpisodeVerification(null);
setAuditTrail([]);
try {
@@ -161,6 +217,26 @@ export default function InsuranceDashboard() {
}
};
+ const handleVerifyEpisode = async () => {
+ if (!walletAddress || !episodeId) return;
+ setIsVerifyingEpisode(true);
+ setVerifyError('');
+ setEpisodeVerification(null);
+ setVerificationResult(null);
+ setAuditTrail([]);
+ try {
+ const parsedEpisodeId = parseInt(episodeId.replace('#', ''), 10);
+ if (isNaN(parsedEpisodeId)) throw new Error('Invalid Episode ID format.');
+ const insurerAddress = account?.address || user?.walletAddress;
+ const res = await verifyEpisodeAsInsurer(insurerAddress, walletAddress, parsedEpisodeId);
+ setEpisodeVerification(res.data);
+ } catch (err) {
+ setVerifyError(err.message || 'Episode verification failed.');
+ } finally {
+ setIsVerifyingEpisode(false);
+ }
+ };
+
return (
@@ -202,6 +278,10 @@ export default function InsuranceDashboard() {
setTokenId(e.target.value)} className="w-full border rounded-xl px-3 py-2.5 text-[12px] font-mono bg-background focus:ring-1 focus:ring-secondary outline-none" />
+
+
+ setEpisodeId(e.target.value)} className="w-full border rounded-xl px-3 py-2.5 text-[12px] font-mono bg-background focus:ring-1 focus:ring-secondary outline-none" />
+
@@ -209,6 +289,12 @@ export default function InsuranceDashboard() {
{isVerifying ? 'Running Cryptographic Checks...' : 'Execute Triple-Check'}
+
+
+ {isVerifyingEpisode ? : }
+ {isVerifyingEpisode ? 'Verifying Episode...' : 'Verify Full Episode'}
+
+
@@ -235,6 +321,12 @@ export default function InsuranceDashboard() {
+
+
{verificationResult.signature && verificationResult.hashMatch ? (
@@ -261,6 +353,52 @@ export default function InsuranceDashboard() {
+ {episodeVerification && (
+
+
+
+ Episode Verification Summary
+
+ Episode #{episodeVerification.episodeId}
+
+
+
+
+
Total
+
{episodeVerification.totalRecords || 0}
+
+
+
Verified
+
{episodeVerification.verifiedCount || 0}
+
+
+
Denied
+
{episodeVerification.deniedCount || 0}
+
+
+
+ {(episodeVerification.records || []).map((row) => (
+
+
+ Record #{row.recordId}
+
+ {row.accessGranted ? 'VERIFIED' : 'DENIED'}
+
+
+ {!row.accessGranted ? (
+
{row.reason}
+ ) : (
+
+ Trust Score: {row.securityChecks?.trustScore ?? 0}
+
+ )}
+
+ ))}
+
+
+
+ )}
+
{auditTrail.length > 0 && (
diff --git a/Frontend/src/components/dashboard/PatientDashboard.jsx b/Frontend/src/components/dashboard/PatientDashboard.jsx
index dbc89da..dac27af 100644
--- a/Frontend/src/components/dashboard/PatientDashboard.jsx
+++ b/Frontend/src/components/dashboard/PatientDashboard.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useActiveAccount, useDisconnect, useActiveWallet } from 'thirdweb/react';
-import { getPatientVault, grantAccess, revokeAccess, checkInToClinic, getActiveGrants, getPatientCheckInStatus, leaveClinic } from '../../services/api';
+import { getPatientVault, grantAccess, revokeAccess, checkInToClinic, getActiveGrants, getPatientCheckInStatus, leaveClinic, getPatientEpisodes } from '../../services/api';
+import QRCode from 'qrcode';
// FIX 1: Alias 'History' to 'HistoryIcon' to prevent "Illegal Constructor" error
import {
@@ -50,7 +51,7 @@ const getCid = (r) => {
const FALLBACK_CHART_DATA = [{ month: 'Jan', v: 0 }, { month: 'Feb', v: 0 }, { month: 'Mar', v: 0 }, { month: 'Apr', v: 0 }, { month: 'May', v: 0 }, { month: 'Jun', v: 0 }, { month: 'Jul', v: 0 }, { month: 'Aug', v: 0 }, { month: 'Sep', v: 0 }, { month: 'Oct', v: 0 }, { month: 'Nov', v: 0 }, { month: 'Dec', v: 0 }];
const NAV = {
- main: [{ id: 'overview', label: 'Overview', icon: LayoutDashboard }, { id: 'records', label: 'My Records', icon: FileText }, { id: 'access', label: 'Access Control', icon: ShieldCheck }, { id: 'appointments', label: 'Appointments', icon: Calendar }],
+ main: [{ id: 'overview', label: 'Overview', icon: LayoutDashboard }, { id: 'records', label: 'My Records', icon: FileText }, { id: 'episodes', label: 'Episodes', icon: ClipboardList }, { id: 'access', label: 'Access Control', icon: ShieldCheck }, { id: 'appointments', label: 'Appointments', icon: Calendar }],
features: [{ id: 'prescriptions', label: 'Prescriptions', icon: ClipboardList }, { id: 'scans', label: 'Scans & Imaging', icon: Bot }, { id: 'notifications', label: 'Notifications', icon: Bell }],
general: [{ id: 'settings', label: 'Settings', icon: Settings }, { id: 'logout', label: 'Log out', icon: LogOut }],
};
@@ -172,6 +173,9 @@ export default function PatientDashboard() {
const [isCheckingIn, setIsCheckingIn] = useState(false);
const [activeCheckIn, setActiveCheckIn] = useState(() => localStorage.getItem('medichain_checkin') || null);
const [activeGrants, setActiveGrants] = useState([]);
+ const [episodes, setEpisodes] = useState(null);
+ const [loadingEpisodes, setLoadingEpisodes] = useState(false);
+ const [qrDataUrl, setQrDataUrl] = useState('');
// --- FIX 3: Robust Viewing Handler ---
const handleViewDocument = (e, rawCid) => {
@@ -196,6 +200,13 @@ export default function PatientDashboard() {
catch (err) { setActiveGrants([]); }
}, []);
+ const fetchEpisodes = useCallback(async () => {
+ setLoadingEpisodes(true);
+ try { const res = await getPatientEpisodes(); setEpisodes(res.data || { episodes: [], ungroupedRecords: [] }); }
+ catch (err) { setEpisodes({ episodes: [], ungroupedRecords: [] }); }
+ finally { setLoadingEpisodes(false); }
+ }, []);
+
const syncCheckInStatus = useCallback(async () => {
try {
const res = await getPatientCheckInStatus();
@@ -217,6 +228,10 @@ export default function PatientDashboard() {
return () => clearInterval(interval);
}, [fetchRecords, fetchGrants, syncCheckInStatus]);
+ useEffect(() => {
+ if (activeNav === 'episodes' && !episodes) fetchEpisodes();
+ }, [activeNav, episodes, fetchEpisodes]);
+
const handleLogout = () => {
if (wallet) disconnect(wallet);
logout();
@@ -257,6 +272,27 @@ export default function PatientDashboard() {
} catch (err) { setErrorMsg(err.message); } finally { setRevokingId(null); }
};
+ const qrPayload = JSON.stringify({
+ type: 'MEDICHAIN_PATIENT',
+ patientAddress: user?.walletAddress || '',
+ issuedAt: new Date().toISOString(),
+ });
+ const copyQrPayload = async () => {
+ try {
+ await navigator.clipboard.writeText(qrPayload);
+ } catch {
+ setErrorMsg('Unable to copy QR payload.');
+ }
+ };
+
+ useEffect(() => {
+ let isMounted = true;
+ QRCode.toDataURL(qrPayload, { width: 180, margin: 1 })
+ .then((url) => { if (isMounted) setQrDataUrl(url); })
+ .catch(() => { if (isMounted) setQrDataUrl(''); });
+ return () => { isMounted = false; };
+ }, [qrPayload]);
+
return (
@@ -340,6 +376,24 @@ export default function PatientDashboard() {
)}
+
+
+
QR MVP (Patient Identity)
+
+
+
+ {qrDataUrl ? (
+

+ ) : (
+
Generating...
+ )}
+
+
Doctor can paste this payload in Doctor Dashboard scan box.
+
{qrPayload}
+
+
+
+
Live Permissions
@@ -394,6 +448,79 @@ export default function PatientDashboard() {
+
+ {/* Episodes of Care View */}
+ {activeNav === 'episodes' && (
+
+
+
+
Episodes of Care
+
+
+
+ {loadingEpisodes ? (
+
+ ) : !episodes ? null : (
+ <>
+ {episodes.episodes && episodes.episodes.length > 0 ? episodes.episodes.map(ep => (
+
+
+
+
+
{ep.title}
+ {ep.description &&
{ep.description}
}
+
+
+ {(ep.records || []).length} record{ep.records?.length !== 1 ? 's' : ''}
+
+
+ Created by: {(ep.createdBy || '').slice(0, 14)}...
+ {ep.records && ep.records.length > 0 ? (
+
+ {ep.records.map(r => (
+
handleViewDocument(e, getCid(r))}>
+
+
{r.recordType || 'Medical Record'}
+
#{r.recordId}
+
+
+ ))}
+
+ ) : (
+ No records linked to this episode yet.
+ )}
+
+
+ )) : (
+
No episodes of care found.
+ )}
+
+ {/* Ungrouped Records */}
+ {episodes.ungroupedRecords && episodes.ungroupedRecords.length > 0 && (
+
+
+
+
+ Ungrouped Records
+ {episodes.ungroupedRecords.length}
+
+
+ {episodes.ungroupedRecords.map(r => (
+
handleViewDocument(e, getCid(r))}>
+
+
{r.recordType || 'Medical Record'}
+
#{r.recordId}
+
+
+ ))}
+
+
+
+ )}
+ >
+ )}
+
+ )}
{
diff --git a/Frontend/src/context/AuthContext.jsx b/Frontend/src/context/AuthContext.jsx
index 6f52b78..9a7d6e1 100644
--- a/Frontend/src/context/AuthContext.jsx
+++ b/Frontend/src/context/AuthContext.jsx
@@ -107,6 +107,17 @@ export function AuthProvider({ children }) {
}
}, [disconnect, wallet]);
+ useEffect(() => {
+ const onUnauthorized = () => {
+ logout();
+ if (window.location.pathname !== '/') {
+ window.location.href = '/';
+ }
+ };
+ window.addEventListener('medichain:unauthorized', onUnauthorized);
+ return () => window.removeEventListener('medichain:unauthorized', onUnauthorized);
+ }, [logout]);
+
const value = { user, token, isAuthenticated, isLoading, showRegister, setShowRegister, login, register, logout, isCorrectNetwork, switchNetwork: () => switchChain(polygonAmoy) };
return {children};
diff --git a/Frontend/src/services/api.js b/Frontend/src/services/api.js
index 0a2fdd5..f992e0a 100644
--- a/Frontend/src/services/api.js
+++ b/Frontend/src/services/api.js
@@ -54,6 +54,12 @@ async function request(method, path, body = null) {
let data; try { data = await res.json(); } catch { data = {}; }
if (!res.ok) {
+ if (res.status === 401) {
+ localStorage.removeItem('medichain_jwt');
+ localStorage.removeItem('medichain_user');
+ window.dispatchEvent(new CustomEvent('medichain:unauthorized'));
+ }
+
let msg = data?.message || data?.error || `Request failed (${res.status})`;
// GLOBAL FIX: Strip out ugly Java stack traces from the UI
@@ -69,10 +75,22 @@ async function request(method, path, body = null) {
return data;
}
-export function requestNonce(walletAddress) { return request('POST', '/api/v1/auth/nonce', { walletAddress }); }
-export function verifySignature(walletAddress, signature) { return request('POST', '/api/v1/auth/verify', { walletAddress, signature }); }
-export function registerUser(name, role) { return request('POST', '/api/v1/users/register', { name, role }); }
-export function getUserProfile(walletAddress) { return request('GET', `/api/v1/users/profile/${walletAddress}`); }
+export async function requestNonce(walletAddress) {
+ const res = await request('POST', '/api/v1/auth/nonce', { walletAddress });
+ return { ...res, messageToSign: res.messageToSign ?? res.data?.messageToSign };
+}
+export async function verifySignature(walletAddress, signature) {
+ const res = await request('POST', '/api/v1/auth/verify', { walletAddress, signature });
+ return { ...res, token: res.token ?? res.data?.token };
+}
+export async function registerUser(name, role) {
+ const res = await request('POST', '/api/v1/users/register', { name, role });
+ return { ...res, user: res.user ?? res.data?.user };
+}
+export async function getUserProfile(walletAddress) {
+ const res = await request('GET', `/api/v1/users/profile/${walletAddress}`);
+ return { ...res, user: res.user ?? res.data?.user };
+}
// --- Patient Methods ---
export async function getPatientVault() {
@@ -80,6 +98,7 @@ export async function getPatientVault() {
return { ...res, data: (res.data || []).map(normalizeRecord) };
}
export function checkInToClinic(doctorAddress) { return request('POST', '/api/v1/dashboard/patient/check-in', { doctorAddress }); }
+export function getPatientEpisodes() { return request('GET', '/api/v1/dashboard/patient/episodes'); }
export async function getActiveGrants() {
const res = await request('GET', '/api/v1/blockchain/active-grants');
@@ -102,6 +121,9 @@ export function leaveClinic() { return request('POST', '/api/v1/dashboard/patien
// --- Doctor Methods ---
export function getWaitingRoom() { return request('GET', '/api/v1/dashboard/doctor/waiting-room'); }
export function completeAppointment(checkInId) { return request('POST', '/api/v1/dashboard/doctor/complete-appointment', { checkInId }); }
+export function createEpisode(patientAddress, title, description = '') {
+ return request('POST', '/api/v1/dashboard/doctor/create-episode', { patientAddress, title, description });
+}
export async function getAccessibleRecords(patientAddress) {
const res = await request('GET', `/api/v1/dashboard/doctor/accessible-records/${normalizeWallet(patientAddress)}`);
@@ -118,8 +140,8 @@ export async function getAccessibleRecords(patientAddress) {
}
// --- Blockchain Methods ---
-export function mintRecord(patientAddress, cid, recordType = 'Medical Record', previousRecordId = null) {
- return request('POST', '/api/v1/blockchain/mint', { patientAddress, cid, recordType, previousRecordId });
+export function mintRecord(patientAddress, cid, recordType = 'Medical Record', previousRecordId = null, episodeId = null) {
+ return request('POST', '/api/v1/blockchain/mint', { patientAddress, cid, recordType, previousRecordId, episodeId });
}
export function amendRecord(patientAddress, cid, previousRecordId, recordType = 'Medical Record') {
@@ -149,3 +171,11 @@ export function viewRecordAsInsurer(insurerAddress, patientAddress, recordId) {
recordId,
});
}
+
+export function verifyEpisodeAsInsurer(insurerAddress, patientAddress, episodeId) {
+ return request('POST', '/api/v1/blockchain/insurer/verify-episode', {
+ insurerAddress,
+ patientAddress,
+ episodeId,
+ });
+}
diff --git a/backend/mvnw b/backend/mvnw
old mode 100644
new mode 100755
diff --git a/backend/src/main/java/org/medichain/backend/config/SecurityConfig.java b/backend/src/main/java/org/medichain/backend/config/SecurityConfig.java
index 4ca368a..e15b32a 100644
--- a/backend/src/main/java/org/medichain/backend/config/SecurityConfig.java
+++ b/backend/src/main/java/org/medichain/backend/config/SecurityConfig.java
@@ -4,6 +4,7 @@
import org.medichain.backend.security.JwtAuthFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -12,6 +13,7 @@
@Configuration
@EnableWebSecurity
+@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
diff --git a/backend/src/main/java/org/medichain/backend/controller/AuthController.java b/backend/src/main/java/org/medichain/backend/controller/AuthController.java
index 16c7416..29f56d7 100644
--- a/backend/src/main/java/org/medichain/backend/controller/AuthController.java
+++ b/backend/src/main/java/org/medichain/backend/controller/AuthController.java
@@ -1,14 +1,13 @@
package org.medichain.backend.controller;
import lombok.extern.slf4j.Slf4j;
+import org.medichain.backend.dto.ApiResponse;
import org.medichain.backend.dto.AuthNonceRequest;
import org.medichain.backend.dto.AuthVerifyRequest;
import org.medichain.backend.service.AuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
-import java.util.Map;
-
@RestController
@RequestMapping("/api/v1/auth")
@CrossOrigin(origins = "*")
@@ -29,12 +28,9 @@ public ResponseEntity> getNonce(@RequestBody AuthNonceRequest request) {
String messageToSign = authService.generateNonce(request.getWalletAddress());
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "messageToSign", messageToSign
- ));
+ return ResponseEntity.ok(ApiResponse.success(java.util.Map.of("messageToSign", messageToSign)));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("NONCE_REQUEST_FAILED", e.getMessage()));
}
}
@@ -49,18 +45,11 @@ public ResponseEntity> verifySignature(@RequestBody AuthVerifyRequest request)
request.getSignature()
);
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "message", "Authentication successful. Welcome to MediChain.",
- "token", jwtToken
- ));
+ return ResponseEntity.ok(ApiResponse.success("Authentication successful. Welcome to MediChain.", java.util.Map.of("token", jwtToken)));
} catch (Exception e) {
log.warn("Auth failed for {}: {}", request.getWalletAddress(), e.getMessage());
// Return 401 Unauthorized if the signature is fake or invalid
- return ResponseEntity.status(401).body(Map.of(
- "status", "error",
- "message", "Authentication failed: Invalid signature or nonce."
- ));
+ return ResponseEntity.status(401).body(ApiResponse.error("AUTH_FAILED", "Authentication failed: Invalid signature or nonce."));
}
}
}
diff --git a/backend/src/main/java/org/medichain/backend/controller/BlockchainController.java b/backend/src/main/java/org/medichain/backend/controller/BlockchainController.java
index e50d4f7..e043acc 100644
--- a/backend/src/main/java/org/medichain/backend/controller/BlockchainController.java
+++ b/backend/src/main/java/org/medichain/backend/controller/BlockchainController.java
@@ -1,13 +1,16 @@
package org.medichain.backend.controller;
import lombok.extern.slf4j.Slf4j;
+import org.medichain.backend.dto.ApiResponse;
import org.medichain.backend.dto.CheckAccessRequest;
import org.medichain.backend.dto.GrantAccessRequest;
+import org.medichain.backend.dto.InsurerVerifyEpisodeRequest;
import org.medichain.backend.dto.InsurerViewRequest;
import org.medichain.backend.dto.MintRecordRequest;
import org.medichain.backend.dto.RevokeAccessRequest;
import org.medichain.backend.service.BlockchainService;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@@ -25,33 +28,33 @@ public BlockchainController(BlockchainService blockchainService) {
}
@PostMapping("/mint")
+ @PreAuthorize("hasRole('DOCTOR')")
public ResponseEntity> mintRecord(@RequestBody MintRecordRequest request) {
try {
- // Pass the recordType to the service
+ // Pass the recordType and optional episodeId to the service
String txHash = blockchainService.mintMedicalRecord(
request.getPatientAddress(),
request.getCid(),
request.getPreviousRecordId(),
- request.getRecordType()
+ request.getRecordType(),
+ request.getEpisodeId()
);
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "transactionHash", txHash
- ));
+ return ResponseEntity.ok(ApiResponse.success(Map.of("transactionHash", txHash)));
}
catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("MINT_FAILED", e.getMessage()));
}
}
@GetMapping("/active-grants")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> getActiveGrants() {
try {
String patientWallet = (String) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
var grants = blockchainService.getActiveGrants(patientWallet);
- return ResponseEntity.ok(Map.of("status", "success", "data", grants));
+ return ResponseEntity.ok(ApiResponse.success(grants));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("ACTIVE_GRANTS_FETCH_FAILED", e.getMessage()));
}
}
@@ -60,32 +63,25 @@ public ResponseEntity> checkAccess(@RequestBody CheckAccessRequest request) {
try {
log.info("Received REST API request to CHECK ACCESS.");
- boolean isAuthorized = blockchainService.checkRecordAccess(
+ boolean isAuthorized = blockchainService.checkRecordAccessWithSqlEnforcement(
request.getPatientAddress(),
request.getDoctorAddress(),
request.getRecordId()
);
if (isAuthorized) {
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "authorized", true,
- "message", "Doctor is authorized to view this record."
- ));
+ return ResponseEntity.ok(ApiResponse.success("Doctor is authorized to view this record.", Map.of("authorized", true)));
} else {
- return ResponseEntity.status(403).body(Map.of(
- "status", "denied",
- "authorized", false,
- "message", "Access denied. The doctor does not have active permission."
- ));
+ return ResponseEntity.status(403).body(ApiResponse.error("ACCESS_DENIED", "Access denied. The doctor does not have active permission."));
}
}
catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("CHECK_ACCESS_FAILED", e.getMessage()));
}
}
@PostMapping("/grant-access")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> grantAccess(@RequestBody GrantAccessRequest request) {
try {
log.info("Received REST API request to GRANT ACCESS.");
@@ -94,18 +90,15 @@ public ResponseEntity> grantAccess(@RequestBody GrantAccessRequest request) {
request.getRecordIds(),
request.getDurationInSeconds()
);
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "message", "Temporary access granted to doctor.",
- "transactionHash", txHash
- ));
+ return ResponseEntity.ok(ApiResponse.success("Temporary access granted to doctor.", Map.of("transactionHash", txHash)));
}
catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("GRANT_ACCESS_FAILED", e.getMessage()));
}
}
@PostMapping("/revoke-access")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> revokeAccess(@RequestBody RevokeAccessRequest request) {
try {
log.info("Received REST API request to REVOKE ACCESS.");
@@ -113,18 +106,15 @@ public ResponseEntity> revokeAccess(@RequestBody RevokeAccessRequest request)
request.getDoctorAddress(),
request.getRecordId()
);
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "message", "Access immediately revoked.",
- "transactionHash", txHash
- ));
+ return ResponseEntity.ok(ApiResponse.success("Access immediately revoked.", Map.of("transactionHash", txHash)));
}
catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("REVOKE_ACCESS_FAILED", e.getMessage()));
}
}
@PostMapping("/insurer/view-record")
+ @PreAuthorize("hasRole('INSURER')")
public ResponseEntity> viewRecordAsInsurer(@RequestBody InsurerViewRequest request) {
try {
log.info("Received REST API request: Insurer View Record via DTO.");
@@ -135,20 +125,33 @@ public ResponseEntity> viewRecordAsInsurer(@RequestBody InsurerViewRequest req
request.getRecordId()
);
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "message", "Fraud check passed. Record retrieved successfully.",
- "data", securePayload
- ));
+ return ResponseEntity.ok(ApiResponse.success("Fraud check passed. Record retrieved successfully.", securePayload));
} catch (Exception e) {
- if ("ACCESS_DENIED".equals(e.getMessage())) {
- return ResponseEntity.status(403).body(Map.of(
- "status", "error",
- "message", "Access Denied. Patient has not granted access or it has expired."
- ));
+ if ("ACCESS_DENIED_SQL_EXPIRED".equals(e.getMessage()) || "ACCESS_DENIED_CHAIN".equals(e.getMessage())) {
+ return ResponseEntity.status(403).body(ApiResponse.error("ACCESS_DENIED", "Access denied. Patient has not granted access or it has expired."));
}
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ if ("RECORD_NOT_FOUND".equals(e.getMessage())) {
+ return ResponseEntity.status(404).body(ApiResponse.error("RECORD_NOT_FOUND", "Record not found."));
+ }
+ return ResponseEntity.internalServerError().body(ApiResponse.error("INSURER_VIEW_FAILED", e.getMessage()));
+ }
+ }
+
+ @PostMapping("/insurer/verify-episode")
+ @PreAuthorize("hasRole('INSURER')")
+ public ResponseEntity> verifyEpisodeAsInsurer(@RequestBody InsurerVerifyEpisodeRequest request) {
+ try {
+ Map payload = blockchainService.verifyEpisodeForInsurer(
+ request.getInsurerAddress(),
+ request.getPatientAddress(),
+ request.getEpisodeId()
+ );
+ return ResponseEntity.ok(ApiResponse.success("Episode verification complete.", payload));
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.badRequest().body(ApiResponse.error("EPISODE_NOT_FOUND", e.getMessage()));
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(ApiResponse.error("VERIFY_EPISODE_FAILED", e.getMessage()));
}
}
}
diff --git a/backend/src/main/java/org/medichain/backend/controller/DashboardController.java b/backend/src/main/java/org/medichain/backend/controller/DashboardController.java
index aec1547..2cd734a 100644
--- a/backend/src/main/java/org/medichain/backend/controller/DashboardController.java
+++ b/backend/src/main/java/org/medichain/backend/controller/DashboardController.java
@@ -1,10 +1,15 @@
package org.medichain.backend.controller;
import lombok.extern.slf4j.Slf4j;
+import org.medichain.backend.dto.ApiResponse;
import org.medichain.backend.dto.CheckInRequest;
import org.medichain.backend.dto.CompleteAppointmentRequest;
+import org.medichain.backend.dto.CreateEpisodeRequest;
+import org.medichain.backend.entity.Episode;
import org.medichain.backend.service.DashboardService;
+import org.medichain.backend.service.EpisodeService;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@@ -15,88 +20,132 @@
@CrossOrigin(origins = "*")
@Slf4j
public class DashboardController {
-
+
private final DashboardService dashboardService;
-
- public DashboardController(DashboardService dashboardService) {
+ private final EpisodeService episodeService;
+
+ public DashboardController(DashboardService dashboardService, EpisodeService episodeService) {
this.dashboardService = dashboardService;
+ this.episodeService = episodeService;
}
-
+
private String getAuthenticatedWallet() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
-
+
// --- PATIENT ENDPOINTS ---
-
+
@GetMapping("/patient/vault")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> getPatientVault() {
try {
var records = dashboardService.getPatientVault(getAuthenticatedWallet());
- return ResponseEntity.ok(Map.of("status", "success", "data", records));
+ return ResponseEntity.ok(ApiResponse.success(records));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("PATIENT_VAULT_FETCH_FAILED", e.getMessage()));
}
}
@PostMapping("/patient/check-in")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> checkInToClinic(@RequestBody CheckInRequest request) {
try {
dashboardService.checkInToClinic(getAuthenticatedWallet(), request.getDoctorAddress());
- return ResponseEntity.ok(Map.of("status", "success", "message", "Checked into clinic waiting room."));
+ return ResponseEntity.ok(ApiResponse.success("Checked into clinic waiting room.", Map.of()));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("CHECKIN_FAILED", e.getMessage()));
}
}
+ @GetMapping("/patient/episodes")
+ @PreAuthorize("hasRole('PATIENT')")
+ public ResponseEntity> getPatientEpisodes() {
+ try {
+ var result = episodeService.getPatientEpisodes(getAuthenticatedWallet());
+ return ResponseEntity.ok(ApiResponse.success(result));
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(ApiResponse.error("PATIENT_EPISODES_FETCH_FAILED", e.getMessage()));
+ }
+ }
+
// --- DOCTOR ENDPOINTS ---
-
+
@GetMapping("/doctor/waiting-room")
+ @PreAuthorize("hasRole('DOCTOR')")
public ResponseEntity> getWaitingRoom() {
try {
var waitingPatients = dashboardService.getWaitingRoom(getAuthenticatedWallet());
- return ResponseEntity.ok(Map.of("status", "success", "data", waitingPatients));
+ return ResponseEntity.ok(ApiResponse.success(waitingPatients));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("WAITING_ROOM_FETCH_FAILED", e.getMessage()));
}
}
@GetMapping("/doctor/accessible-records/{patientAddress}")
+ @PreAuthorize("hasRole('DOCTOR')")
public ResponseEntity> getAccessibleRecords(@PathVariable String patientAddress) {
try {
var activeGrants = dashboardService.getAccessibleRecordsForPatient(getAuthenticatedWallet(), patientAddress);
- return ResponseEntity.ok(Map.of("status", "success", "data", activeGrants));
+ return ResponseEntity.ok(ApiResponse.success(activeGrants));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("ACCESSIBLE_RECORDS_FETCH_FAILED", e.getMessage()));
}
}
@PostMapping("/doctor/complete-appointment")
+ @PreAuthorize("hasRole('DOCTOR')")
public ResponseEntity> completeAppointment(@RequestBody CompleteAppointmentRequest request) {
try {
dashboardService.completeAppointment(getAuthenticatedWallet(), request.getCheckInId());
- return ResponseEntity.ok(Map.of("status", "success", "message", "Appointment completed."));
+ return ResponseEntity.ok(ApiResponse.success("Appointment completed.", Map.of()));
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(ApiResponse.error("COMPLETE_APPOINTMENT_FAILED", e.getMessage()));
+ }
+ }
+
+ @PostMapping("/doctor/create-episode")
+ @PreAuthorize("hasRole('DOCTOR')")
+ public ResponseEntity> createEpisode(@RequestBody CreateEpisodeRequest request) {
+ try {
+ String doctorWallet = getAuthenticatedWallet();
+ Episode episode = episodeService.createEpisode(
+ request.getPatientAddress(),
+ request.getTitle(),
+ request.getDescription(),
+ doctorWallet
+ );
+ return ResponseEntity.ok(ApiResponse.success(Map.of(
+ "episodeId", episode.getEpisodeId(),
+ "patientAddress", episode.getPatientAddress(),
+ "title", episode.getTitle(),
+ "description", episode.getDescription() != null ? episode.getDescription() : "",
+ "createdBy", episode.getCreatedBy(),
+ "createdAt", episode.getCreatedAt().toString()
+ )));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("CREATE_EPISODE_FAILED", e.getMessage()));
}
}
@GetMapping("/patient/check-in-status")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> getCheckInStatus() {
try {
var checkIn = dashboardService.getPatientActiveCheckIn(getAuthenticatedWallet());
- return ResponseEntity.ok(Map.of("status", "success", "data", checkIn != null ? checkIn : ""));
+ return ResponseEntity.ok(ApiResponse.success(checkIn != null ? checkIn : ""));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("CHECKIN_STATUS_FETCH_FAILED", e.getMessage()));
}
}
@PostMapping("/patient/leave-room")
+ @PreAuthorize("hasRole('PATIENT')")
public ResponseEntity> leaveWaitingRoom() {
try {
dashboardService.leaveWaitingRoom(getAuthenticatedWallet());
- return ResponseEntity.ok(Map.of("status", "success", "message", "Left the waiting room."));
+ return ResponseEntity.ok(ApiResponse.success("Left the waiting room.", Map.of()));
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("LEAVE_WAITING_ROOM_FAILED", e.getMessage()));
}
}
}
diff --git a/backend/src/main/java/org/medichain/backend/controller/UserController.java b/backend/src/main/java/org/medichain/backend/controller/UserController.java
index 683fea3..9307102 100644
--- a/backend/src/main/java/org/medichain/backend/controller/UserController.java
+++ b/backend/src/main/java/org/medichain/backend/controller/UserController.java
@@ -1,6 +1,7 @@
package org.medichain.backend.controller;
import lombok.extern.slf4j.Slf4j;
+import org.medichain.backend.dto.ApiResponse;
import org.medichain.backend.dto.UserRegistrationRequest;
import org.medichain.backend.entity.User;
import org.medichain.backend.repository.UserRepository;
@@ -8,7 +9,6 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
-import java.util.Map;
import java.util.Optional;
@RestController
@@ -41,9 +41,9 @@ public ResponseEntity> completeRegistration(@RequestBody UserRegistrationReque
// 3. Prevent already registered users from overwriting their role
if (!"UNREGISTERED".equals(user.getRole())) {
- return ResponseEntity.badRequest().body(Map.of(
- "status", "error",
- "message", "Profile is already fully registered as a " + user.getRole()
+ return ResponseEntity.badRequest().body(ApiResponse.error(
+ "USER_ALREADY_REGISTERED",
+ "Profile is already fully registered as a " + user.getRole()
));
}
@@ -54,15 +54,11 @@ public ResponseEntity> completeRegistration(@RequestBody UserRegistrationReque
userRepository.save(user);
log.info("Profile Completed: {} is now registered as a {}", user.getName(), user.getRole());
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "message", "Welcome to MediChain, " + user.getName(),
- "user", user
- ));
+ return ResponseEntity.ok(ApiResponse.success("Welcome to MediChain, " + user.getName(), java.util.Map.of("user", user)));
} catch (Exception e) {
log.error("Registration error: {}", e.getMessage());
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("USER_REGISTRATION_FAILED", e.getMessage()));
}
}
@@ -77,18 +73,12 @@ public ResponseEntity> getUserProfile(@PathVariable String walletAddress) {
Optional userOpt = userRepository.findByWalletAddressIgnoreCase(walletAddress);
if (userOpt.isPresent()) {
- return ResponseEntity.ok(Map.of(
- "status", "success",
- "user", userOpt.get()
- ));
+ return ResponseEntity.ok(ApiResponse.success(java.util.Map.of("user", userOpt.get())));
} else {
- return ResponseEntity.status(404).body(Map.of(
- "status", "error",
- "message", "User not found."
- ));
+ return ResponseEntity.status(404).body(ApiResponse.error("USER_NOT_FOUND", "User not found."));
}
} catch (Exception e) {
- return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
+ return ResponseEntity.internalServerError().body(ApiResponse.error("USER_PROFILE_FETCH_FAILED", e.getMessage()));
}
}
}
diff --git a/backend/src/main/java/org/medichain/backend/dto/ApiResponse.java b/backend/src/main/java/org/medichain/backend/dto/ApiResponse.java
new file mode 100644
index 0000000..73f7a2f
--- /dev/null
+++ b/backend/src/main/java/org/medichain/backend/dto/ApiResponse.java
@@ -0,0 +1,32 @@
+package org.medichain.backend.dto;
+
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public final class ApiResponse {
+ private ApiResponse() {}
+
+ public static Map success(Object data) {
+ Map body = new LinkedHashMap<>();
+ body.put("status", "success");
+ body.put("data", data);
+ body.put("timestamp", Instant.now().toString());
+ return body;
+ }
+
+ public static Map success(String message, Object data) {
+ Map body = success(data);
+ body.put("message", message);
+ return body;
+ }
+
+ public static Map error(String errorCode, String message) {
+ Map body = new LinkedHashMap<>();
+ body.put("status", "error");
+ body.put("errorCode", errorCode);
+ body.put("message", message);
+ body.put("timestamp", Instant.now().toString());
+ return body;
+ }
+}
diff --git a/backend/src/main/java/org/medichain/backend/dto/CreateEpisodeRequest.java b/backend/src/main/java/org/medichain/backend/dto/CreateEpisodeRequest.java
new file mode 100644
index 0000000..baed932
--- /dev/null
+++ b/backend/src/main/java/org/medichain/backend/dto/CreateEpisodeRequest.java
@@ -0,0 +1,10 @@
+package org.medichain.backend.dto;
+
+import lombok.Data;
+
+@Data
+public class CreateEpisodeRequest {
+ private String patientAddress;
+ private String title;
+ private String description; // optional
+}
diff --git a/backend/src/main/java/org/medichain/backend/dto/InsurerVerifyEpisodeRequest.java b/backend/src/main/java/org/medichain/backend/dto/InsurerVerifyEpisodeRequest.java
new file mode 100644
index 0000000..09a1998
--- /dev/null
+++ b/backend/src/main/java/org/medichain/backend/dto/InsurerVerifyEpisodeRequest.java
@@ -0,0 +1,10 @@
+package org.medichain.backend.dto;
+
+import lombok.Data;
+
+@Data
+public class InsurerVerifyEpisodeRequest {
+ private String insurerAddress;
+ private String patientAddress;
+ private Long episodeId;
+}
diff --git a/backend/src/main/java/org/medichain/backend/dto/MintRecordRequest.java b/backend/src/main/java/org/medichain/backend/dto/MintRecordRequest.java
index 97c1da8..3a444dd 100644
--- a/backend/src/main/java/org/medichain/backend/dto/MintRecordRequest.java
+++ b/backend/src/main/java/org/medichain/backend/dto/MintRecordRequest.java
@@ -8,4 +8,5 @@ public class MintRecordRequest {
private String cid; // The IPFS hash of the medical file
private Long previousRecordId; // If amending, the record ID being superseded (null for new records)
private String recordType;
+ private Long episodeId; // Optional: link to an episode
}
diff --git a/backend/src/main/java/org/medichain/backend/entity/Episode.java b/backend/src/main/java/org/medichain/backend/entity/Episode.java
new file mode 100644
index 0000000..fa6607d
--- /dev/null
+++ b/backend/src/main/java/org/medichain/backend/entity/Episode.java
@@ -0,0 +1,39 @@
+package org.medichain.backend.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "episodes")
+@Data
+public class Episode {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "episode_id")
+ private Long episodeId;
+
+ @Column(name = "patient_address", nullable = false, length = 42)
+ private String patientAddress;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "description")
+ private String description;
+
+ @Column(name = "created_by", nullable = false, length = 42)
+ private String createdBy;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDateTime createdAt;
+
+ @PrePersist
+ protected void onCreate() {
+ if (this.createdAt == null) {
+ this.createdAt = LocalDateTime.now();
+ }
+ }
+}
diff --git a/backend/src/main/java/org/medichain/backend/entity/MedicalRecord.java b/backend/src/main/java/org/medichain/backend/entity/MedicalRecord.java
index b482ca5..1d065c0 100644
--- a/backend/src/main/java/org/medichain/backend/entity/MedicalRecord.java
+++ b/backend/src/main/java/org/medichain/backend/entity/MedicalRecord.java
@@ -32,4 +32,7 @@ public class MedicalRecord {
@Column(name = "tx_hash", nullable = false)
private String txHash; // To prove to the user that it's actually on the blockchain
+
+ @Column(name = "episode_id")
+ private Long episodeId; // Nullable FK to episodes(episode_id)
}
diff --git a/backend/src/main/java/org/medichain/backend/repository/AccessGrantRepository.java b/backend/src/main/java/org/medichain/backend/repository/AccessGrantRepository.java
index e087b22..d0e1ec3 100644
--- a/backend/src/main/java/org/medichain/backend/repository/AccessGrantRepository.java
+++ b/backend/src/main/java/org/medichain/backend/repository/AccessGrantRepository.java
@@ -6,7 +6,6 @@
import java.time.LocalDateTime;
import java.util.List;
-import java.util.Optional;
import java.util.UUID;
@Repository
@@ -27,4 +26,7 @@ List findByPatientAddressIgnoreCaseAndViewerAddressIgnoreCaseAndRec
// Fetch active grants for a SPECIFIC patient and doctor combination
List findByViewerAddressIgnoreCaseAndPatientAddressIgnoreCaseAndExpiresAtAfter(
String viewerAddress, String patientAddress, LocalDateTime currentTime);
+
+ boolean existsByPatientAddressIgnoreCaseAndViewerAddressIgnoreCaseAndRecordIdAndExpiresAtAfter(
+ String patientAddress, String viewerAddress, Long recordId, LocalDateTime currentTime);
}
diff --git a/backend/src/main/java/org/medichain/backend/repository/EpisodeRepository.java b/backend/src/main/java/org/medichain/backend/repository/EpisodeRepository.java
new file mode 100644
index 0000000..2d1cfd8
--- /dev/null
+++ b/backend/src/main/java/org/medichain/backend/repository/EpisodeRepository.java
@@ -0,0 +1,12 @@
+package org.medichain.backend.repository;
+
+import org.medichain.backend.entity.Episode;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface EpisodeRepository extends JpaRepository {
+ List findByPatientAddressIgnoreCaseOrderByCreatedAtDesc(String patientAddress);
+}
diff --git a/backend/src/main/java/org/medichain/backend/repository/MedicalRecordRepository.java b/backend/src/main/java/org/medichain/backend/repository/MedicalRecordRepository.java
index 5ae5169..67d7769 100644
--- a/backend/src/main/java/org/medichain/backend/repository/MedicalRecordRepository.java
+++ b/backend/src/main/java/org/medichain/backend/repository/MedicalRecordRepository.java
@@ -15,4 +15,6 @@ public interface MedicalRecordRepository extends JpaRepository findByDoctorAddressIgnoreCase(String doctorAddress);
List findByDoctorAddressIgnoreCaseAndPatientAddressIgnoreCase(String doctorAddress, String patientAddress);
+
+ List findByPatientAddressIgnoreCaseAndEpisodeIdOrderByRecordIdDesc(String patientAddress, Long episodeId);
}
diff --git a/backend/src/main/java/org/medichain/backend/security/JwtAuthEntryPoint.java b/backend/src/main/java/org/medichain/backend/security/JwtAuthEntryPoint.java
index 4149529..04303fa 100644
--- a/backend/src/main/java/org/medichain/backend/security/JwtAuthEntryPoint.java
+++ b/backend/src/main/java/org/medichain/backend/security/JwtAuthEntryPoint.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import org.medichain.backend.dto.ApiResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@@ -14,6 +15,8 @@ public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Returns 401 instead of 403
- response.getWriter().write("{\"status\": \"error\", \"message\": \"Unauthorized. A valid JWT token is required.\"}");
+ response.getWriter().write(new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(
+ ApiResponse.error("UNAUTHORIZED", "Unauthorized. A valid JWT token is required.")
+ ));
}
}
diff --git a/backend/src/main/java/org/medichain/backend/service/BlockchainService.java b/backend/src/main/java/org/medichain/backend/service/BlockchainService.java
index e02941f..a2eb853 100644
--- a/backend/src/main/java/org/medichain/backend/service/BlockchainService.java
+++ b/backend/src/main/java/org/medichain/backend/service/BlockchainService.java
@@ -7,6 +7,7 @@
import org.medichain.backend.entity.MedicalRecord;
import org.medichain.backend.repository.AccessGrantRepository;
import org.medichain.backend.repository.MedicalRecordRepository;
+import org.medichain.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@@ -25,10 +26,15 @@
@Service
@Slf4j
public class BlockchainService {
+ private static final int TRUST_SCORE_PROVIDER = 40;
+ private static final int TRUST_SCORE_INTEGRITY = 40;
+ private static final int TRUST_SCORE_LATEST = 20;
+ private static final int TRUST_SCORE_SUPERSEDED = 10;
private final Web3j web3j;
private final MedicalRecordRepository medicalRecordRepository;
private final AccessGrantRepository accessGrantRepository;
+ private final UserRepository userRepository;
private MedRecordNFT smartContract;
@@ -40,10 +46,12 @@ public class BlockchainService {
public BlockchainService(Web3j web3j,
MedicalRecordRepository medicalRecordRepository,
- AccessGrantRepository accessGrantRepository) {
+ AccessGrantRepository accessGrantRepository,
+ UserRepository userRepository) {
this.web3j = web3j;
this.medicalRecordRepository = medicalRecordRepository;
this.accessGrantRepository = accessGrantRepository;
+ this.userRepository = userRepository;
}
@PostConstruct
@@ -75,7 +83,7 @@ private String getAuthenticatedWalletAddress() {
}
@Transactional
- public String mintMedicalRecord(String patientWalletAddress, String ipfsCid, Long previousRecordId, String recordType) {
+ public String mintMedicalRecord(String patientWalletAddress, String ipfsCid, Long previousRecordId, String recordType, Long episodeId) {
try {
String doctorAddress = getAuthenticatedWalletAddress();
@@ -102,6 +110,7 @@ public String mintMedicalRecord(String patientWalletAddress, String ipfsCid, Lon
record.setSuperseded(false);
record.setPreviousRecordId(previousRecordId);
record.setTxHash(transactionHash);
+ record.setEpisodeId(episodeId); // nullable — null when not provided
medicalRecordRepository.save(record);
return transactionHash;
@@ -185,46 +194,150 @@ public boolean checkRecordAccess(String patientAddress, String doctorAddress, Lo
throw new RuntimeException("Failed to read from blockchain", e);
}
}
+
+ public boolean checkRecordAccessWithSqlEnforcement(String patientAddress, String viewerAddress, Long recordId) {
+ if (!hasActiveSqlGrant(patientAddress, viewerAddress, recordId)) {
+ return false;
+ }
+ return checkRecordAccess(patientAddress, viewerAddress, recordId);
+ }
public Map getVerifiedRecordForInsurer(String insurerAddress, String patientAddress, Long recordId) {
-
try {
log.info("Insurer {} requesting access to Record ID: {}", insurerAddress, recordId);
+ if (!hasActiveSqlGrant(patientAddress, insurerAddress, recordId)) {
+ throw new RuntimeException("ACCESS_DENIED_SQL_EXPIRED");
+ }
boolean hasAccess = checkRecordAccess(patientAddress, insurerAddress, recordId);
- if (!hasAccess) throw new RuntimeException("ACCESS_DENIED");
-
- var onChainRecord = smartContract.records(BigInteger.valueOf(recordId)).send();
- String cid = onChainRecord.component1();
- String issuingDoctor = onChainRecord.component3();
- BigInteger previousTokenId = onChainRecord.component4();
- boolean isSuperseded = onChainRecord.component5();
-
- boolean isAuthenticityValid = issuingDoctor != null && !issuingDoctor.startsWith("0x0000000000000000");
- String auditStatus = isSuperseded ? "WARNING: This record has been AMENDED." : "VALID: Latest Version";
-
- log.info("Verification Complete. Returning secure metadata.");
-
+ if (!hasAccess) throw new RuntimeException("ACCESS_DENIED_CHAIN");
+
+ MedicalRecord localRecord = medicalRecordRepository.findById(recordId)
+ .orElseThrow(() -> new RuntimeException("RECORD_NOT_FOUND"));
+ Map securityChecks = buildSecurityChecks(localRecord, recordId);
List history = getFullAuditTrail(recordId);
-
+ Map recordData = (Map) securityChecks.get("recordData");
+
return Map.of(
"accessGranted", true,
- "recordData", Map.of(
- "ipfsCid", cid,
- "issuingDoctor", issuingDoctor,
- "isSuperseded", isSuperseded,
- "previousRecordId", previousTokenId,
- "auditStatus", auditStatus
- ),
- "securityChecks", Map.of(
- "authenticityVerified", isAuthenticityValid,
- "integrityVerified", true
- ),
- "auditTrail", history // <-- We attached the Linked List history here!
+ "recordData", recordData,
+ "securityChecks", securityChecks,
+ "auditTrail", history
);
} catch (Exception e) {
log.error("Error fetching record for insurer: {}", e.getMessage());
- throw new RuntimeException(e);
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public Map verifyEpisodeForInsurer(String insurerAddress, String patientAddress, Long episodeId) {
+ List records = medicalRecordRepository.findByPatientAddressIgnoreCaseAndEpisodeIdOrderByRecordIdDesc(
+ patientAddress, episodeId
+ );
+ if (records.isEmpty()) {
+ throw new IllegalArgumentException("No records found for this episode.");
+ }
+
+ List