Création de la feature API V1
This commit is contained in:
Generated
+477
@@ -22,10 +22,60 @@
|
|||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/philsturgeon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "14.0.1",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.1.0",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-draft-04": "^1.0.0",
|
||||||
|
"call-me-maybe": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
@@ -501,6 +551,15 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/cliui": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||||
@@ -569,6 +628,13 @@
|
|||||||
"@otplib/core": "13.4.1"
|
"@otplib/core": "13.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
|
||||||
@@ -578,6 +644,12 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -600,6 +672,36 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-draft-04": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.5.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -630,12 +732,27 @@
|
|||||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -752,6 +869,18 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||||
|
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
@@ -837,6 +966,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
@@ -903,6 +1038,15 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||||
@@ -989,6 +1133,20 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -1065,6 +1223,18 @@
|
|||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/doctrine": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -1166,6 +1336,15 @@
|
|||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -1245,6 +1424,28 @@
|
|||||||
"express": ">= 4.11"
|
"express": ">= 4.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
@@ -1282,6 +1483,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/foreground-child": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
|
"signal-exit": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1376,6 +1593,30 @@
|
|||||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||||
|
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.3.1",
|
||||||
|
"jackspeak": "^4.1.1",
|
||||||
|
"minimatch": "^10.1.1",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -1509,6 +1750,55 @@
|
|||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/jackspeak": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.3",
|
"version": "9.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
@@ -1606,12 +1896,27 @@
|
|||||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.once": {
|
"node_modules/lodash.once": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "11.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||||
|
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1693,6 +1998,21 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
@@ -1702,6 +2022,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
@@ -1860,6 +2189,13 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/otplib": {
|
"node_modules/otplib": {
|
||||||
"version": "13.4.1",
|
"version": "13.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/otplib/-/otplib-13.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/otplib/-/otplib-13.4.1.tgz",
|
||||||
@@ -1910,6 +2246,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/package-json-from-dist": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1928,6 +2270,31 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-key": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||||
@@ -2100,6 +2467,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-main-filename": {
|
"node_modules/require-main-filename": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
@@ -2245,6 +2621,27 @@
|
|||||||
"@img/sharp-win32-x64": "0.34.5"
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/shebang-command": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"shebang-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shebang-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
@@ -2317,6 +2714,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
@@ -2441,6 +2850,50 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-jsdoc": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "^12.1.0",
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "11.1.0",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.32.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz",
|
||||||
|
"integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
||||||
@@ -2563,6 +3016,21 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/node-which"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which-module": {
|
"node_modules/which-module": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
@@ -2643,6 +3111,15 @@
|
|||||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "15.4.1",
|
"version": "15.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2254,4 +2254,29 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_objectifs_investisseur ON objectifs(inve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Migration : table api_keys ───────────────────────────────────────────
|
||||||
|
// Clés API générées par l'utilisateur pour un accès programmatique/MCP.
|
||||||
|
// Chaque clé est scopée à un investisseur précis (usage single-user
|
||||||
|
// multi-investisseur — pas de notion multi-tenant ici). Seul le hash SHA-256
|
||||||
|
// est stocké ; la valeur en clair n'est montrée qu'une fois à la création.
|
||||||
|
// `key_prefix` (8 premiers caractères) permet d'identifier une clé dans
|
||||||
|
// l'UI sans jamais réafficher la valeur complète.
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||||
|
nom TEXT NOT NULL,
|
||||||
|
key_prefix TEXT NOT NULL,
|
||||||
|
key_hash TEXT NOT NULL UNIQUE,
|
||||||
|
scopes TEXT NOT NULL DEFAULT 'read',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
last_used_at TEXT,
|
||||||
|
revoked_at TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)');
|
||||||
|
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_inv ON api_keys(investisseur_id)');
|
||||||
|
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)');
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import db from '../db/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentification par clé API (X-API-Key), distincte du JWT utilisé par le
|
||||||
|
* frontend (requireAuth). Une clé API est toujours scopée à un seul
|
||||||
|
* investisseur — pas de notion de "scope=all" ici, contrairement aux routes
|
||||||
|
* internes. Réservé aux routes /api/v1 (lecture seule, Phase 1).
|
||||||
|
*/
|
||||||
|
export function requireApiKey(req, res, next) {
|
||||||
|
const key = req.header('X-API-Key');
|
||||||
|
if (!key) {
|
||||||
|
return res.status(401).json({ error: 'Missing X-API-Key header' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
||||||
|
const row = db.prepare(`
|
||||||
|
SELECT k.id, k.investisseur_id, k.scopes, k.revoked_at
|
||||||
|
FROM api_keys k
|
||||||
|
WHERE k.key_hash = ?
|
||||||
|
`).get(hash);
|
||||||
|
|
||||||
|
if (!row || row.revoked_at) {
|
||||||
|
return res.status(401).json({ error: 'Invalid or revoked API key' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare(`UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?`).run(row.id);
|
||||||
|
|
||||||
|
req.apiKeyId = row.id;
|
||||||
|
req.investisseurId = row.investisseur_id;
|
||||||
|
req.apiScopes = (row.scopes || 'read').split(',').map(s => s.trim());
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import db from '../db/index.js';
|
||||||
|
import { HttpError } from '../middleware/errorHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
const KEY_PREFIX_LEN = 12; // ex: "clk_live_ab3" — assez pour identifier sans exposer le secret
|
||||||
|
|
||||||
|
function generateKey() {
|
||||||
|
const secret = crypto.randomBytes(24).toString('hex'); // 48 caractères hex
|
||||||
|
const full = `clk_live_${secret}`;
|
||||||
|
const hash = crypto.createHash('sha256').update(full).digest('hex');
|
||||||
|
return { full, hash, prefix: full.slice(0, KEY_PREFIX_LEN) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GET /api/api-keys ── liste des clés de l'utilisateur connecté ──────── */
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||||
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||||
|
FROM api_keys k
|
||||||
|
JOIN investisseurs i ON i.id = k.investisseur_id
|
||||||
|
WHERE k.user_id = ?
|
||||||
|
ORDER BY k.revoked_at IS NOT NULL, k.created_at DESC
|
||||||
|
`).all(req.user.id);
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── POST /api/api-keys ── créer une nouvelle clé (nom + investisseur) ──── */
|
||||||
|
router.post('/', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const nom = (req.body?.nom || '').trim();
|
||||||
|
const investisseur_id = Number(req.body?.investisseur_id);
|
||||||
|
|
||||||
|
if (!nom) throw new HttpError(400, 'Le nom de la clé est requis');
|
||||||
|
if (nom.length > 100) throw new HttpError(400, 'Le nom de la clé est trop long (100 caractères max)');
|
||||||
|
if (!Number.isInteger(investisseur_id)) throw new HttpError(400, 'investisseur_id est requis');
|
||||||
|
|
||||||
|
const inv = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?')
|
||||||
|
.get(investisseur_id, req.user.id);
|
||||||
|
if (!inv) throw new HttpError(404, 'Investisseur introuvable');
|
||||||
|
|
||||||
|
const { full, hash, prefix } = generateKey();
|
||||||
|
|
||||||
|
const info = db.prepare(`
|
||||||
|
INSERT INTO api_keys (user_id, investisseur_id, nom, key_prefix, key_hash, scopes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'read')
|
||||||
|
`).run(req.user.id, investisseur_id, nom, prefix, hash);
|
||||||
|
|
||||||
|
const saved = db.prepare(`
|
||||||
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||||
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||||
|
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
||||||
|
WHERE k.id = ?
|
||||||
|
`).get(info.lastInsertRowid);
|
||||||
|
|
||||||
|
// La valeur en clair n'est renvoyée qu'ici, une seule fois.
|
||||||
|
res.status(201).json({ ...saved, key: full });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── PATCH /api/api-keys/:id ── renommer une clé ─────────────────────────── */
|
||||||
|
router.patch('/:id', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const nom = (req.body?.nom || '').trim();
|
||||||
|
if (!nom) throw new HttpError(400, 'Le nom de la clé est requis');
|
||||||
|
if (nom.length > 100) throw new HttpError(400, 'Le nom de la clé est trop long (100 caractères max)');
|
||||||
|
|
||||||
|
const existing = db.prepare('SELECT id FROM api_keys WHERE id = ? AND user_id = ?')
|
||||||
|
.get(req.params.id, req.user.id);
|
||||||
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
||||||
|
|
||||||
|
db.prepare('UPDATE api_keys SET nom = ? WHERE id = ?').run(nom, req.params.id);
|
||||||
|
|
||||||
|
const saved = db.prepare(`
|
||||||
|
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||||
|
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||||
|
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
||||||
|
WHERE k.id = ?
|
||||||
|
`).get(req.params.id);
|
||||||
|
res.json(saved);
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── DELETE /api/api-keys/:id ── révoque la clé (soft-delete) ───────────── */
|
||||||
|
router.delete('/:id', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const existing = db.prepare('SELECT id, revoked_at FROM api_keys WHERE id = ? AND user_id = ?')
|
||||||
|
.get(req.params.id, req.user.id);
|
||||||
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
||||||
|
if (existing.revoked_at) return res.json({ revoked: true });
|
||||||
|
|
||||||
|
db.prepare(`UPDATE api_keys SET revoked_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
||||||
|
res.json({ revoked: true });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── DELETE /api/api-keys/:id/purge ── suppression définitive de la ligne ──
|
||||||
|
Distinct de la révocation ci-dessus : ici la clé disparaît complètement
|
||||||
|
(active ou déjà révoquée). Le frontend affiche un avertissement avant
|
||||||
|
d'appeler cette route si la clé est encore active. ─────────────────── */
|
||||||
|
router.delete('/:id/purge', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const existing = db.prepare('SELECT id FROM api_keys WHERE id = ? AND user_id = ?')
|
||||||
|
.get(req.params.id, req.user.id);
|
||||||
|
if (!existing) throw new HttpError(404, 'Clé API introuvable');
|
||||||
|
|
||||||
|
db.prepare('DELETE FROM api_keys WHERE id = ?').run(req.params.id);
|
||||||
|
res.json({ deleted: true });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../../db/index.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /dashboard:
|
||||||
|
* get:
|
||||||
|
* summary: Synthèse du portefeuille (KPIs)
|
||||||
|
* tags: [Dashboard]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* responses:
|
||||||
|
* 200: { description: Synthèse KPI }
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const invId = req.investisseurId;
|
||||||
|
|
||||||
|
const investissements = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS nb_investissements,
|
||||||
|
COALESCE(SUM(montant_investi), 0) AS total_investi,
|
||||||
|
COALESCE(SUM(CASE WHEN statut='en_cours' THEN montant_investi END), 0) AS encours,
|
||||||
|
COALESCE(SUM(CASE WHEN statut='rembourse' THEN montant_investi END), 0) AS rembourse
|
||||||
|
FROM investissements WHERE investisseur_id = ?
|
||||||
|
`).get(invId);
|
||||||
|
|
||||||
|
const interets = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
|
||||||
|
COALESCE(SUM(r.interets_nets), 0) AS interets_nets,
|
||||||
|
COALESCE(SUM(r.capital), 0) AS capital_recu,
|
||||||
|
COALESCE(SUM(r.net_recu), 0) AS net_recu_total
|
||||||
|
FROM remboursements r
|
||||||
|
JOIN investissements i ON i.id = r.investissement_id
|
||||||
|
WHERE i.investisseur_id = ?
|
||||||
|
`).get(invId);
|
||||||
|
|
||||||
|
const cash = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN type='depot' THEN montant END), 0) AS total_depots,
|
||||||
|
COALESCE(SUM(CASE WHEN type='retrait' THEN montant END), 0) AS total_retraits
|
||||||
|
FROM depots_retraits WHERE investisseur_id = ?
|
||||||
|
`).get(invId);
|
||||||
|
|
||||||
|
res.json({ investissements, interets, cash });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../../db/index.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /depots-retraits:
|
||||||
|
* get:
|
||||||
|
* summary: Liste des mouvements de cash (dépôts / retraits)
|
||||||
|
* tags: [Dépôts / Retraits]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* responses:
|
||||||
|
* 200: { description: Liste des mouvements }
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT dr.id, dr.date_operation, p.nom AS plateforme_nom, dr.type,
|
||||||
|
dr.montant, dr.libelle
|
||||||
|
FROM depots_retraits dr
|
||||||
|
JOIN plateformes p ON p.id = dr.plateforme_id
|
||||||
|
WHERE dr.investisseur_id = ?
|
||||||
|
ORDER BY dr.date_operation DESC
|
||||||
|
`).all(req.investisseurId);
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import investisseurRouter from './investisseur.js';
|
||||||
|
import investissementsRouter from './investissements.js';
|
||||||
|
import remboursementsRouter from './remboursements.js';
|
||||||
|
import depotsRetraitsRouter from './depotsRetraits.js';
|
||||||
|
import dashboardRouter from './dashboard.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use('/investisseur', investisseurRouter);
|
||||||
|
router.use('/investissements', investissementsRouter);
|
||||||
|
router.use('/remboursements', remboursementsRouter);
|
||||||
|
router.use('/depots-retraits', depotsRetraitsRouter);
|
||||||
|
router.use('/dashboard', dashboardRouter);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../../db/index.js';
|
||||||
|
import { HttpError } from '../../middleware/errorHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
const LIST_COLUMNS = `
|
||||||
|
i.id, i.nom_projet, i.emetteur, p.nom AS plateforme_nom,
|
||||||
|
i.date_souscription, i.date_premiere_echeance, i.date_cible,
|
||||||
|
i.montant_investi, i.taux_interet, i.duree_mois,
|
||||||
|
i.type_remb, i.freq_interets, i.statut, i.reference
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /investissements:
|
||||||
|
* get:
|
||||||
|
* summary: Liste des investissements de l'investisseur
|
||||||
|
* tags: [Investissements]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: statut
|
||||||
|
* schema: { type: string, enum: [en_cours, rembourse, en_retard, procedure, cloture] }
|
||||||
|
* responses:
|
||||||
|
* 200: { description: Liste des investissements }
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const { statut } = req.query;
|
||||||
|
const conds = ['i.investisseur_id = ?'];
|
||||||
|
const args = [req.investisseurId];
|
||||||
|
if (statut) { conds.push('i.statut = ?'); args.push(statut); }
|
||||||
|
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT ${LIST_COLUMNS}
|
||||||
|
FROM investissements i
|
||||||
|
JOIN plateformes p ON p.id = i.plateforme_id
|
||||||
|
WHERE ${conds.join(' AND ')}
|
||||||
|
ORDER BY i.date_souscription DESC
|
||||||
|
`).all(...args);
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /investissements/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Détail d'un investissement, avec ses remboursements réels
|
||||||
|
* tags: [Investissements]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema: { type: integer }
|
||||||
|
* responses:
|
||||||
|
* 200: { description: Détail de l'investissement }
|
||||||
|
* 404: { description: Investissement introuvable }
|
||||||
|
*/
|
||||||
|
router.get('/:id', (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const inv = db.prepare(`
|
||||||
|
SELECT ${LIST_COLUMNS}, i.notes
|
||||||
|
FROM investissements i
|
||||||
|
JOIN plateformes p ON p.id = i.plateforme_id
|
||||||
|
WHERE i.id = ? AND i.investisseur_id = ?
|
||||||
|
`).get(req.params.id, req.investisseurId);
|
||||||
|
if (!inv) throw new HttpError(404, 'Investissement introuvable');
|
||||||
|
|
||||||
|
const remboursements = db.prepare(`
|
||||||
|
SELECT id, date_remb, capital, interets_bruts, prelev_sociaux,
|
||||||
|
prelev_forfaitaire, cashback, interets_nets, net_recu, statut
|
||||||
|
FROM remboursements
|
||||||
|
WHERE investissement_id = ?
|
||||||
|
ORDER BY date_remb
|
||||||
|
`).all(inv.id);
|
||||||
|
|
||||||
|
res.json({ ...inv, remboursements });
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../../db/index.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /investisseur:
|
||||||
|
* get:
|
||||||
|
* summary: Profil de l'investisseur lié à la clé API
|
||||||
|
* tags: [Investisseur]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Profil investisseur
|
||||||
|
* 401:
|
||||||
|
* description: Clé API invalide ou manquante
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const inv = db.prepare(`
|
||||||
|
SELECT id, nom, prenom, type, type_fiscal, notes, created_at
|
||||||
|
FROM investisseurs WHERE id = ?
|
||||||
|
`).get(req.investisseurId);
|
||||||
|
res.json(inv);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../../db/index.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /remboursements:
|
||||||
|
* get:
|
||||||
|
* summary: Liste des remboursements réels perçus
|
||||||
|
* tags: [Remboursements]
|
||||||
|
* security: [{ ApiKeyAuth: [] }]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: date_debut
|
||||||
|
* schema: { type: string, format: date }
|
||||||
|
* - in: query
|
||||||
|
* name: date_fin
|
||||||
|
* schema: { type: string, format: date }
|
||||||
|
* responses:
|
||||||
|
* 200: { description: Liste des remboursements }
|
||||||
|
*/
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
const { date_debut, date_fin } = req.query;
|
||||||
|
const conds = ['i.investisseur_id = ?'];
|
||||||
|
const args = [req.investisseurId];
|
||||||
|
if (date_debut) { conds.push('r.date_remb >= ?'); args.push(date_debut); }
|
||||||
|
if (date_fin) { conds.push('r.date_remb <= ?'); args.push(date_fin); }
|
||||||
|
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT r.id, r.date_remb, i.nom_projet, p.nom AS plateforme_nom,
|
||||||
|
r.capital, r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire,
|
||||||
|
r.cashback, r.interets_nets, r.net_recu, r.statut
|
||||||
|
FROM remboursements r
|
||||||
|
JOIN investissements i ON i.id = r.investissement_id
|
||||||
|
JOIN plateformes p ON p.id = i.plateforme_id
|
||||||
|
WHERE ${conds.join(' AND ')}
|
||||||
|
ORDER BY r.date_remb DESC
|
||||||
|
`).all(...args);
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -52,6 +52,10 @@ import secteursInvRouter from './routes/secteurs-inv.js';
|
|||||||
import associationsInvRouter from './routes/associations-inv.js';
|
import associationsInvRouter from './routes/associations-inv.js';
|
||||||
import notificationsRouter from './routes/notifications.js';
|
import notificationsRouter from './routes/notifications.js';
|
||||||
import ticketsRouter from './routes/tickets.js';
|
import ticketsRouter from './routes/tickets.js';
|
||||||
|
import apiKeysRouter from './routes/apiKeys.js';
|
||||||
|
import v1Router from './routes/v1/index.js';
|
||||||
|
import { requireApiKey } from './middleware/apiKey.js';
|
||||||
|
import { swaggerSpec, swaggerUi } from './swagger.js';
|
||||||
import db from './db/index.js';
|
import db from './db/index.js';
|
||||||
import { getSmtpConfig } from './utils/mailer.js';
|
import { getSmtpConfig } from './utils/mailer.js';
|
||||||
|
|
||||||
@@ -104,6 +108,15 @@ app.get('/api/app-info', (_, res) => {
|
|||||||
|
|
||||||
app.use('/api/auth', authLimiter, authRouter);
|
app.use('/api/auth', authLimiter, authRouter);
|
||||||
|
|
||||||
|
// ── API publique v1 (lecture seule, authentification par clé API) ────────
|
||||||
|
// Montée ici, avant les routes internes ci-dessous : la ligne
|
||||||
|
// `app.use('/api', requireAuth, associationsInvRouter)` plus bas capte tout
|
||||||
|
// préfixe /api/* avec le JWT interne, ce qui intercepterait /api/v1/* et
|
||||||
|
// /api/docs si elles étaient montées après.
|
||||||
|
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||||
|
app.get('/api/openapi.json', (_, res) => res.json(swaggerSpec));
|
||||||
|
app.use('/api/v1', requireApiKey, v1Router);
|
||||||
|
|
||||||
// All routes below require authentication
|
// All routes below require authentication
|
||||||
app.use('/api/investisseurs', requireAuth, investisseursRouter);
|
app.use('/api/investisseurs', requireAuth, investisseursRouter);
|
||||||
app.use('/api/plateformes', requireAuth, plateformesRouter);
|
app.use('/api/plateformes', requireAuth, plateformesRouter);
|
||||||
@@ -142,6 +155,7 @@ app.use('/api/secteurs-inv', requireAuth, secteursInvRouter);
|
|||||||
app.use('/api', requireAuth, associationsInvRouter);
|
app.use('/api', requireAuth, associationsInvRouter);
|
||||||
app.use('/api/notifications', notificationsRouter);
|
app.use('/api/notifications', notificationsRouter);
|
||||||
app.use('/api/tickets', requireAuth, ticketsRouter);
|
app.use('/api/tickets', requireAuth, ticketsRouter);
|
||||||
|
app.use('/api/api-keys', requireAuth, apiKeysRouter);
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import swaggerJsdoc from 'swagger-jsdoc';
|
||||||
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
// swagger-jsdoc résout ses motifs `apis` avec `glob`, qui n'interprète pas
|
||||||
|
// les antislashs Windows comme séparateurs — on force donc des slashs.
|
||||||
|
const toGlobPath = (p) => p.split(path.sep).join('/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documentation OpenAPI de l'API publique v1 (lecture seule).
|
||||||
|
* Générée depuis les annotations JSDoc `@openapi` dans backend/src/routes/v1/*.js.
|
||||||
|
* Servie sur /api/docs (public, pas d'authentification pour consulter la doc —
|
||||||
|
* seules les requêtes réelles vers /api/v1/... nécessitent une clé API).
|
||||||
|
*/
|
||||||
|
const swaggerSpec = swaggerJsdoc({
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.3',
|
||||||
|
info: {
|
||||||
|
title: 'Crowdlending Tracker API',
|
||||||
|
version: 'v1',
|
||||||
|
description:
|
||||||
|
"API publique en lecture seule du portefeuille de crowdlending. " +
|
||||||
|
"Authentification par clé API (header `X-API-Key`), générée depuis Mon compte → Clés API. " +
|
||||||
|
"Chaque clé est scopée à un seul investisseur.",
|
||||||
|
},
|
||||||
|
servers: [{ url: '/api/v1' }],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
ApiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'X-API-Key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [{ ApiKeyAuth: [] }],
|
||||||
|
},
|
||||||
|
apis: [toGlobPath(path.join(__dirname, 'routes/v1/*.js'))],
|
||||||
|
});
|
||||||
|
|
||||||
|
const nbPaths = Object.keys(swaggerSpec.paths || {}).length;
|
||||||
|
console.log(`[Swagger] ${nbPaths} route(s) documentée(s) sur /api/docs`);
|
||||||
|
|
||||||
|
export { swaggerSpec, swaggerUi };
|
||||||
@@ -4,7 +4,10 @@ import PasswordInput from '../components/PasswordInput.jsx';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { useUi } from '../context/UiContext.jsx';
|
import { useUi } from '../context/UiContext.jsx';
|
||||||
|
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
|
||||||
|
import Modal from '../components/Modal.jsx';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
|
import { memberLabel } from '../utils/format.js';
|
||||||
|
|
||||||
/* ── Icônes nav ─────────────────────────────────────────────── */
|
/* ── Icônes nav ─────────────────────────────────────────────── */
|
||||||
function IconUser() {
|
function IconUser() {
|
||||||
@@ -13,6 +16,12 @@ function IconUser() {
|
|||||||
function IconLock() {
|
function IconLock() {
|
||||||
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>;
|
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>;
|
||||||
}
|
}
|
||||||
|
function IconKey() {
|
||||||
|
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="7.5" cy="15.5" r="5.5"/><path d="M21 2l-9.6 9.6"/><path d="M15.5 7.5l3 3L22 7l-3-3"/></svg>;
|
||||||
|
}
|
||||||
|
function IconTrash() {
|
||||||
|
return <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Dropdown custom style Finary ────────────────────────────── */
|
/* ── Dropdown custom style Finary ────────────────────────────── */
|
||||||
const LANGUES = [
|
const LANGUES = [
|
||||||
@@ -782,6 +791,302 @@ function DeleteAccountSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Clés API ─────────────────────────────────────────────────
|
||||||
|
Permet de générer des clés pour un accès programmatique (API publique
|
||||||
|
en lecture seule, serveur MCP local...). Chaque clé est nommée par
|
||||||
|
l'utilisateur et rattachée à un investisseur précis. La valeur en clair
|
||||||
|
n'est affichée qu'une seule fois, juste après la création. ─────────── */
|
||||||
|
function NewApiKeyModal({ open, onClose, onCreated, investisseurs }) {
|
||||||
|
const [nom, setNom] = useState('');
|
||||||
|
const [investisseurId, setInvestisseurId] = useState(investisseurs[0]?.id || '');
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [err, setErr] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setNom('');
|
||||||
|
setInvestisseurId(investisseurs[0]?.id || '');
|
||||||
|
setErr(null);
|
||||||
|
}
|
||||||
|
}, [open, investisseurs]);
|
||||||
|
|
||||||
|
const submit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!nom.trim()) return setErr('Le nom de la clé est requis');
|
||||||
|
if (!investisseurId) return setErr('Sélectionnez un investisseur');
|
||||||
|
setBusy(true);
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
const created = await api.post('/api-keys', { nom: nom.trim(), investisseur_id: Number(investisseurId) });
|
||||||
|
onCreated(created);
|
||||||
|
} catch (e) { setErr(e.message); }
|
||||||
|
finally { setBusy(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} title="Nouvelle clé API" onClose={onClose}>
|
||||||
|
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{err && <div className="error">{err}</div>}
|
||||||
|
<div>
|
||||||
|
<label>Nom de la clé</label>
|
||||||
|
<input value={nom} onChange={e => setNom(e.target.value)}
|
||||||
|
placeholder="ex : MCP local, Script import…" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Investisseur</label>
|
||||||
|
<select value={investisseurId} onChange={e => setInvestisseurId(e.target.value)}>
|
||||||
|
{investisseurs.map(i => (
|
||||||
|
<option key={i.id} value={i.id}>{memberLabel(i)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted" style={{ margin: 0, fontSize: 'var(--fs-sm)' }}>
|
||||||
|
La clé donne un accès en lecture seule aux données de cet investisseur. Elle ne sera affichée en clair qu'une seule fois.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4 }}>
|
||||||
|
<button type="button" className="ghost" onClick={onClose}>Annuler</button>
|
||||||
|
<button type="submit" className="primary" disabled={busy}>
|
||||||
|
{busy ? 'Création…' : 'Créer la clé'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL absolue de la doc Swagger, déduite de VITE_API_URL comme le reste de l'app
|
||||||
|
* (ex: pages/settings/PlateformesSection.jsx). En dev via proxy Vite, VITE_API_URL
|
||||||
|
* n'est pas défini et l'URL reste relative — ça fonctionne aussi bien. */
|
||||||
|
const DOCS_URL = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/docs';
|
||||||
|
|
||||||
|
function RevealApiKeyModal({ apiKey, onClose }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copiedCurl, setCopiedCurl] = useState(false);
|
||||||
|
if (!apiKey) return null;
|
||||||
|
|
||||||
|
const curlCmd = `curl -H "X-API-Key: ${apiKey.key}" <URL_DE_VOTRE_API>/api/v1/dashboard`;
|
||||||
|
|
||||||
|
const copy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(apiKey.key);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch { /* clipboard indisponible, l'utilisateur peut sélectionner manuellement */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyCurl = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(curlCmd);
|
||||||
|
setCopiedCurl(true);
|
||||||
|
setTimeout(() => setCopiedCurl(false), 2000);
|
||||||
|
} catch { /* clipboard indisponible */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={!!apiKey} title="Clé API créée" onClose={onClose}>
|
||||||
|
<p style={{ margin: '0 0 12px', fontSize: 'var(--fs-sm)' }}>
|
||||||
|
Copiez cette clé maintenant : elle ne sera plus jamais affichée en entier.
|
||||||
|
</p>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px',
|
||||||
|
borderRadius: 8, border: '1px solid var(--border)', background: 'var(--surface-2, #f9fafb)',
|
||||||
|
fontFamily: 'monospace', fontSize: 13, wordBreak: 'break-all',
|
||||||
|
}}>
|
||||||
|
<span style={{ flex: 1 }}>{apiKey.key}</span>
|
||||||
|
<button type="button" className="ghost" onClick={copy} style={{ flexShrink: 0 }}>
|
||||||
|
{copied ? 'Copié ✓' : 'Copier'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)' }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px', fontSize: 'var(--fs-sm)' }}>Tester votre clé</h4>
|
||||||
|
<p style={{ margin: '0 0 8px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||||
|
Le plus simple : ouvrez la <a href={DOCS_URL} target="_blank" rel="noreferrer">documentation interactive (Swagger)</a>,
|
||||||
|
cliquez sur « Authorize » et collez la clé ci-dessus, puis testez un endpoint (ex. <code>GET /dashboard</code>).
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: '0 0 6px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||||
|
Ou en ligne de commande (remplacez <code><URL_DE_VOTRE_API></code> par l'adresse de votre backend,
|
||||||
|
ex. <code>http://localhost:4000</code> en dev) :
|
||||||
|
</p>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
|
||||||
|
borderRadius: 8, border: '1px solid var(--border)', background: 'var(--surface-2, #f9fafb)',
|
||||||
|
fontFamily: 'monospace', fontSize: 12, wordBreak: 'break-all',
|
||||||
|
}}>
|
||||||
|
<span style={{ flex: 1 }}>{curlCmd}</span>
|
||||||
|
<button type="button" className="ghost" onClick={copyCurl} style={{ flexShrink: 0 }}>
|
||||||
|
{copiedCurl ? 'Copié ✓' : 'Copier'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||||
|
<button type="button" className="primary" onClick={onClose}>J'ai copié la clé</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtKeyDate(iso) {
|
||||||
|
if (!iso) return '—';
|
||||||
|
return new Date(iso.replace(' ', 'T') + 'Z').toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modale d'avertissement avant suppression définitive — affichée uniquement
|
||||||
|
* quand la clé est encore active (une clé révoquée ne sert déjà plus à rien,
|
||||||
|
* pas besoin d'avertissement dans ce cas). */
|
||||||
|
function DeleteApiKeyModal({ apiKey, onCancel, onConfirm, busy }) {
|
||||||
|
if (!apiKey) return null;
|
||||||
|
return (
|
||||||
|
<Modal open={!!apiKey} title="Supprimer une clé active ?" onClose={onCancel}>
|
||||||
|
<p style={{ margin: '0 0 8px', fontSize: 'var(--fs-sm)' }}>
|
||||||
|
La clé « {apiKey.nom} » est encore <strong>active</strong>. La supprimer coupera immédiatement l'accès
|
||||||
|
à toute application qui l'utilise (API, serveur MCP...), et cette action est irréversible.
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: 0, fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||||
|
Si vous voulez juste bloquer l'accès sans supprimer la clé, préférez « Révoquer ».
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
|
||||||
|
<button type="button" className="ghost" onClick={onCancel} disabled={busy}>Annuler</button>
|
||||||
|
<button type="button" className="primary" onClick={onConfirm} disabled={busy}
|
||||||
|
style={{ background: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}>
|
||||||
|
{busy ? 'Suppression…' : 'Supprimer définitivement'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiKeysSection() {
|
||||||
|
const { investisseurs } = useInvestisseur();
|
||||||
|
const [keys, setKeys] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [err, setErr] = useState(null);
|
||||||
|
const [showNew, setShowNew] = useState(false);
|
||||||
|
const [revealKey, setRevealKey] = useState(null);
|
||||||
|
const [busyId, setBusyId] = useState(null);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState(null); // clé active en attente de confirmation
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await api.get('/api-keys');
|
||||||
|
setKeys(data);
|
||||||
|
} catch (e) { setErr(e.message); }
|
||||||
|
finally { setLoading(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
const revoke = async (id) => {
|
||||||
|
if (!window.confirm('Révoquer cette clé ? Toute application qui l\'utilise perdra immédiatement l\'accès.')) return;
|
||||||
|
setBusyId(id);
|
||||||
|
try {
|
||||||
|
await api.del(`/api-keys/${id}`);
|
||||||
|
await load();
|
||||||
|
} catch (e) { setErr(e.message); }
|
||||||
|
finally { setBusyId(null); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const purge = async (id) => {
|
||||||
|
setBusyId(id);
|
||||||
|
try {
|
||||||
|
await api.del(`/api-keys/${id}/purge`);
|
||||||
|
setDeleteTarget(null);
|
||||||
|
await load();
|
||||||
|
} catch (e) { setErr(e.message); }
|
||||||
|
finally { setBusyId(null); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Clé active → avertissement avant suppression. Clé révoquée → suppression directe. */
|
||||||
|
const handleDeleteClick = (k) => {
|
||||||
|
if (k.revoked_at) purge(k.id);
|
||||||
|
else setDeleteTarget(k);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ marginTop: 20 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||||
|
<h3 style={{ margin: 0 }}>Clés API</h3>
|
||||||
|
<button className="primary" onClick={() => setShowNew(true)} disabled={!investisseurs.length}>
|
||||||
|
+ Nouvelle clé
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
|
||||||
|
Utilisées pour un accès programmatique en lecture seule (API publique, serveur MCP local...). Vous pouvez créer plusieurs clés, une par usage.
|
||||||
|
{' '}Consultez la <a href={DOCS_URL} target="_blank" rel="noreferrer">documentation de l'API (Swagger)</a> pour la liste des endpoints disponibles.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{err && <div className="error" style={{ marginBottom: 12 }}>{err}</div>}
|
||||||
|
|
||||||
|
{!loading && keys.length === 0 && (
|
||||||
|
<p style={{ color: 'var(--text-muted)', fontSize: 13 }}>Aucune clé API pour l'instant.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{keys.length > 0 && (
|
||||||
|
<table style={{ width: '100%' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Investisseur</th>
|
||||||
|
<th>Clé</th>
|
||||||
|
<th>Créée le</th>
|
||||||
|
<th>Dernière utilisation</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{keys.map(k => (
|
||||||
|
<tr key={k.id}>
|
||||||
|
<td>{k.nom}</td>
|
||||||
|
<td>{k.investisseur_nom}</td>
|
||||||
|
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{k.key_prefix}…</td>
|
||||||
|
<td>{fmtKeyDate(k.created_at)}</td>
|
||||||
|
<td>{fmtKeyDate(k.last_used_at)}</td>
|
||||||
|
<td>
|
||||||
|
{k.revoked_at
|
||||||
|
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--danger-bg, #fef2f2)', color: 'var(--danger, #dc2626)' }}>Révoquée</span>
|
||||||
|
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)' }}>Active</span>}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 4 }}>
|
||||||
|
{!k.revoked_at && (
|
||||||
|
<button className="ghost" onClick={() => revoke(k.id)} disabled={busyId === k.id}
|
||||||
|
style={{ color: 'var(--danger,#dc2626)' }}>
|
||||||
|
Révoquer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="ghost" onClick={() => handleDeleteClick(k)} disabled={busyId === k.id}
|
||||||
|
title="Supprimer définitivement"
|
||||||
|
style={{ color: 'var(--text-muted)', padding: '6px 8px' }}>
|
||||||
|
<IconTrash />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NewApiKeyModal
|
||||||
|
open={showNew}
|
||||||
|
onClose={() => setShowNew(false)}
|
||||||
|
investisseurs={investisseurs}
|
||||||
|
onCreated={(created) => { setShowNew(false); setRevealKey(created); load(); }}
|
||||||
|
/>
|
||||||
|
<RevealApiKeyModal apiKey={revealKey} onClose={() => setRevealKey(null)} />
|
||||||
|
<DeleteApiKeyModal
|
||||||
|
apiKey={deleteTarget}
|
||||||
|
busy={busyId === deleteTarget?.id}
|
||||||
|
onCancel={() => setDeleteTarget(null)}
|
||||||
|
onConfirm={() => purge(deleteTarget.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Page principale ─────────────────────────────────────────── */
|
/* ── Page principale ─────────────────────────────────────────── */
|
||||||
export default function MonCompte() {
|
export default function MonCompte() {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
@@ -794,6 +1099,7 @@ export default function MonCompte() {
|
|||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
{ id: 'profil', label: 'Mon compte', icon: <IconUser /> },
|
{ id: 'profil', label: 'Mon compte', icon: <IconUser /> },
|
||||||
{ id: 'securite', label: 'Sécurité', icon: <IconLock /> },
|
{ id: 'securite', label: 'Sécurité', icon: <IconLock /> },
|
||||||
|
{ id: 'api-keys', label: 'Clés API', icon: <IconKey /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -818,6 +1124,7 @@ export default function MonCompte() {
|
|||||||
<div className="account-content-narrow">
|
<div className="account-content-narrow">
|
||||||
{section === 'profil' && <><AccountForm /><DeleteAccountSection /></>}
|
{section === 'profil' && <><AccountForm /><DeleteAccountSection /></>}
|
||||||
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
|
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
|
||||||
|
{section === 'api-keys' && <ApiKeysSection />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user