Modification des pages d'authentification
This commit is contained in:
Generated
+398
@@ -19,6 +19,8 @@
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^8.0.11",
|
||||
"otplib": "^13.4.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"sharp": "^0.34.5",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8"
|
||||
@@ -499,6 +501,83 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/core": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.4.1.tgz",
|
||||
"integrity": "sha512-KIXgK1hNtWJEBMTastbe1bpmuais+3f+ATeO8TkMs2rNkfGO1FbQy8+/UWVEu3TR/iTJerU0idkPudaPmLP2BA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@otplib/hotp": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.4.1.tgz",
|
||||
"integrity": "sha512-g9q04SwpG5ZtMnVkUcgcoAlwCH4YLROZN1qhyBwgkBzqYYVSYhpP6gSGaxGHwePLt1c+e6NqDlgIZN+e1/XPuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.4.1",
|
||||
"@otplib/uri": "13.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/plugin-base32-scure": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.4.1.tgz",
|
||||
"integrity": "sha512-Fs/r5qisC05SRhT6xWXaypB6PVC0vgWf6zztmi0J5RnQ09OJiPDWCJFH6cDm6ANsrdvB9di7X+Jb7L13BoEbUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.4.1",
|
||||
"@scure/base": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/plugin-crypto-noble": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.4.1.tgz",
|
||||
"integrity": "sha512-PJfVW8/1hdS6CfxLheKPZSLTwDq4TijZbN4yRjxlv0ODdzmxpM+wGwWr1JXMdy0xJPxLziydQD5gdVqrR4/gAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@otplib/core": "13.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/totp": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.4.1.tgz",
|
||||
"integrity": "sha512-QOkBVPrf6AM4qZaReZPSk9/I8ATVdZpIISJz115MqeVtcrbcr5llPZ0J7804tpnjnp1vCRkI5Qjd47HhgVteBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.4.1",
|
||||
"@otplib/hotp": "13.4.1",
|
||||
"@otplib/uri": "13.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/uri": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.4.1.tgz",
|
||||
"integrity": "sha512-xaIm7bvICMhoB2rZIR5luiaMdssWR5nY5nXnR1fdezUgZuEO58D6zrGzLp7pQuBmlpmL0HagnscDQFoskp9yiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
|
||||
"integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -521,6 +600,30 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"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/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
@@ -731,6 +834,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
@@ -750,6 +862,17 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"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/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
@@ -759,6 +882,24 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
@@ -854,6 +995,15 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -906,6 +1056,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -947,6 +1103,12 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -1104,6 +1266,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1146,6 +1321,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -1307,6 +1491,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -1362,6 +1555,18 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"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/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -1652,6 +1857,56 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/otplib": {
|
||||
"version": "13.4.1",
|
||||
"resolved": "https://registry.npmjs.org/otplib/-/otplib-13.4.1.tgz",
|
||||
"integrity": "sha512-o5CxfDw6bh7hoDv0NUUIcc0RqzJ9ipfUrzeKheKJ+vs4rXZnDlA9n4a/7R1cDjpmLjKLix4BgNVRmoDkm5rLSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.4.1",
|
||||
"@otplib/hotp": "13.4.1",
|
||||
"@otplib/plugin-base32-scure": "13.4.1",
|
||||
"@otplib/plugin-crypto-noble": "13.4.1",
|
||||
"@otplib/totp": "13.4.1",
|
||||
"@otplib/uri": "13.4.1"
|
||||
}
|
||||
},
|
||||
"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/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/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1661,12 +1916,30 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -1723,6 +1996,23 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"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",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
@@ -1798,6 +2088,21 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1881,6 +2186,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -2092,6 +2403,32 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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/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/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@@ -2223,6 +2560,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
@@ -2241,6 +2584,20 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -2277,6 +2634,47 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"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/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/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/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^8.0.11",
|
||||
"otplib": "^13.4.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"sharp": "^0.34.5",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
+101
-76
@@ -939,7 +939,6 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_corrections_plateforme ON corrections_s
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
|
||||
// ── Table user_preferences ───────────────────────────────────────────────────
|
||||
// Stockage générique des préférences UI par utilisateur.
|
||||
@@ -1671,85 +1670,111 @@ db.exec(`
|
||||
console.log('[DB] Tables catégories/secteurs plateforme+investissement OK');
|
||||
}
|
||||
|
||||
// ── Migration ponctuelle : correction date_cible aberrantes (>2100) ──────────
|
||||
// Certains prêts différés importés ont une date_cible avec un siècle erroné.
|
||||
// On recalcule date_souscription + duree_mois et on régénère la simulation.
|
||||
|
||||
// ── Migration : table de configuration SMTP ──────────────────────────────────
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS smtp_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
host TEXT NOT NULL DEFAULT '',
|
||||
port INTEGER NOT NULL DEFAULT 587,
|
||||
secure INTEGER NOT NULL DEFAULT 0,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
allow_unauth INTEGER NOT NULL DEFAULT 0,
|
||||
app_name TEXT NOT NULL DEFAULT 'Crowdlending Tracker',
|
||||
app_url TEXT NOT NULL DEFAULT '',
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// ── Migration : email_verified sur users ─────────────────────────────────────
|
||||
{
|
||||
function fixAddMonths(isoDate, months) {
|
||||
const [y, m, d] = isoDate.split('-').map(Number);
|
||||
let nm = m + months;
|
||||
let ny = y;
|
||||
while (nm > 12) { nm -= 12; ny++; }
|
||||
const maxDay = new Date(Date.UTC(ny, nm, 0)).getUTCDate();
|
||||
const nd = Math.min(d, maxDay);
|
||||
return `${String(ny).padStart(4,'0')}-${String(nm).padStart(2,'0')}-${String(nd).padStart(2,'0')}`;
|
||||
}
|
||||
|
||||
const toFix = db.prepare(`
|
||||
SELECT i.id, i.date_souscription, i.duree_mois,
|
||||
i.montant_investi, i.taux_interet, i.type_remb, i.freq_interets,
|
||||
i.date_premiere_echeance, i.date_debut_simul, i.echeance_fin_de_mois
|
||||
FROM investissements i
|
||||
WHERE i.statut IN ('en_cours','en_retard','procedure')
|
||||
AND i.type_remb = 'differe'
|
||||
AND i.date_cible > '2100-01-01'
|
||||
AND i.duree_mois IS NOT NULL
|
||||
`).all();
|
||||
|
||||
if (toFix.length > 0) {
|
||||
const updateDate = db.prepare(`UPDATE investissements SET date_cible=?, updated_at=datetime('now') WHERE id=?`);
|
||||
const fixAll = db.transaction(() => {
|
||||
for (const inv of toFix) {
|
||||
const newDate = fixAddMonths(inv.date_souscription, inv.duree_mois);
|
||||
updateDate.run(newDate, inv.id);
|
||||
generateSimul(db, { ...inv, date_cible: newDate });
|
||||
console.log(`[DB] Fix date_cible id=${inv.id} → ${newDate}`);
|
||||
}
|
||||
});
|
||||
fixAll();
|
||||
console.log(`[DB] ${toFix.length} date_cible aberrantes corrigées.`);
|
||||
const userCols = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
|
||||
if (!userCols.includes('email_verified')) {
|
||||
// DEFAULT 1 pour ne pas bloquer les comptes existants
|
||||
db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 1');
|
||||
console.log('[DB] users.email_verified ajouté');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Migration : table smtp_config ─────────────────────────────────────────
|
||||
// ── Migration : table email_verification_tokens ───────────────────────────────
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS email_verification_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// ── Migration : table password_reset_tokens ───────────────────────────────────
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// ── Migrations 2FA ────────────────────────────────────────────────────────────
|
||||
{
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS smtp_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
host TEXT,
|
||||
port INTEGER NOT NULL DEFAULT 587,
|
||||
secure INTEGER NOT NULL DEFAULT 0,
|
||||
email TEXT,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
allow_unauth INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Seed row unique (id=1) si elle n'existe pas encore
|
||||
const existing = db.prepare('SELECT id FROM smtp_config WHERE id = 1').get();
|
||||
if (!existing) {
|
||||
// Pré-remplir depuis les variables d'environnement si disponibles
|
||||
db.prepare(`
|
||||
INSERT INTO smtp_config (id, enabled, host, port, email, username, password)
|
||||
VALUES (1, 0, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
process.env.SMTP_HOST || null,
|
||||
parseInt(process.env.SMTP_PORT || '587', 10),
|
||||
process.env.SMTP_EMAIL || null,
|
||||
process.env.SMTP_USERNAME || null,
|
||||
process.env.SMTP_PASSWORD || null,
|
||||
);
|
||||
const userCols2 = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
|
||||
if (!userCols2.includes('totp_secret')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN totp_secret TEXT');
|
||||
console.log('[DB] users.totp_secret ajouté');
|
||||
}
|
||||
if (!userCols2.includes('totp_enabled')) {
|
||||
db.exec('ALTER TABLE users ADD COLUMN totp_enabled INTEGER NOT NULL DEFAULT 0');
|
||||
console.log('[DB] users.totp_enabled ajouté');
|
||||
}
|
||||
|
||||
// Ajout des colonnes app_name et app_url si absentes
|
||||
const smtpCols = db.prepare('PRAGMA table_info(smtp_config)').all().map(c => c.name);
|
||||
if (!smtpCols.includes('app_name'))
|
||||
db.exec(`ALTER TABLE smtp_config ADD COLUMN app_name TEXT DEFAULT 'Crowdlending'`);
|
||||
if (!smtpCols.includes('app_url'))
|
||||
db.exec(`ALTER TABLE smtp_config ADD COLUMN app_url TEXT DEFAULT ''`);
|
||||
|
||||
console.log('[DB] Table smtp_config OK');
|
||||
}
|
||||
|
||||
// Sessions temporaires 2FA (entre /login et /2fa/verify)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS two_fa_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_2fa_sess_token ON two_fa_sessions(token)');
|
||||
|
||||
// Codes OTP envoyés par email
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS two_fa_email_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
code TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_2fa_email_uid ON two_fa_email_codes(user_id)');
|
||||
|
||||
// Appareils de confiance (30 jours)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS two_fa_trusted_devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_2fa_dev_token ON two_fa_trusted_devices(token)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_2fa_dev_uid ON two_fa_trusted_devices(user_id)');
|
||||
|
||||
console.log('[DB] Migrations 2FA OK');
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -48,13 +48,24 @@ const router = Router();
|
||||
/** Liste tous les utilisateurs */
|
||||
router.get('/users', (req, res) => {
|
||||
const users = db.prepare(`
|
||||
SELECT id, email, display_name, role, created_at
|
||||
SELECT id, email, display_name, role, email_verified, created_at
|
||||
FROM users
|
||||
ORDER BY id ASC
|
||||
`).all();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
/** Vérifier manuellement l'email d'un utilisateur */
|
||||
router.patch('/users/:id/verify-email', (req, res, next) => {
|
||||
try {
|
||||
const targetId = Number(req.params.id);
|
||||
const r = db.prepare("UPDATE users SET email_verified=1, updated_at=datetime('now') WHERE id=?").run(targetId);
|
||||
if (r.changes === 0) throw new HttpError(404, 'Utilisateur introuvable');
|
||||
db.prepare('UPDATE email_verification_tokens SET used=1 WHERE user_id=? AND used=0').run(targetId);
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
/** Crée un utilisateur */
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -387,7 +398,7 @@ router.get('/smtp', (req, res) => {
|
||||
username: row.username || '',
|
||||
hasPassword: !!(row.password),
|
||||
allowUnauth: !!row.allow_unauth,
|
||||
appName: row.app_name || 'Crowdlending',
|
||||
appName: row.app_name || 'Crowdlending Tracker',
|
||||
appUrl: row.app_url || '',
|
||||
});
|
||||
});
|
||||
@@ -450,7 +461,7 @@ router.put('/smtp', (req, res, next) => {
|
||||
body.username ?? null,
|
||||
passwordToStore,
|
||||
body.allowUnauth ? 1 : 0,
|
||||
body.appName ?? 'Crowdlending',
|
||||
body.appName ?? 'Crowdlending Tracker',
|
||||
body.appUrl ?? '',
|
||||
);
|
||||
|
||||
|
||||
+406
-18
@@ -1,9 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'node:crypto';
|
||||
import { z } from 'zod';
|
||||
import db from '../db/index.js';
|
||||
import { signToken, requireAuth } from '../middleware/auth.js';
|
||||
import { HttpError } from '../middleware/errorHandler.js';
|
||||
import { sendMail, buildEmailHtml, getSmtpConfig } from '../utils/mailer.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -18,7 +20,7 @@ const LoginSchema = z.object({
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
router.post('/register', (req, res, next) => {
|
||||
router.post('/register', async (req, res, next) => {
|
||||
try {
|
||||
const body = RegisterSchema.parse(req.body);
|
||||
const exists = db.prepare('SELECT id FROM users WHERE email = ?').get(body.email);
|
||||
@@ -28,10 +30,15 @@ router.post('/register', (req, res, next) => {
|
||||
const isFirst = db.prepare('SELECT COUNT(*) AS n FROM users').get().n === 0;
|
||||
const role = isFirst ? 'admin' : 'user';
|
||||
|
||||
// Le 1er utilisateur (admin) est auto-vérifié ; les suivants si SMTP désactivé
|
||||
const cfg = getSmtpConfig();
|
||||
const smtpReady = cfg.enabled && cfg.host && cfg.email;
|
||||
const autoVerified = isFirst || !smtpReady ? 1 : 0;
|
||||
|
||||
const hash = bcrypt.hashSync(body.password, 10);
|
||||
const result = db
|
||||
.prepare('INSERT INTO users (email, password_hash, display_name, role) VALUES (?, ?, ?, ?)')
|
||||
.run(body.email, hash, body.displayName || null, role);
|
||||
.prepare('INSERT INTO users (email, password_hash, display_name, role, email_verified) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(body.email, hash, body.displayName || null, role, autoVerified);
|
||||
|
||||
const userId = result.lastInsertRowid;
|
||||
|
||||
@@ -47,36 +54,104 @@ router.post('/register', (req, res, next) => {
|
||||
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
|
||||
).run(userId, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
|
||||
|
||||
// Si l'utilisateur doit vérifier son email → envoyer l'email de bienvenue
|
||||
if (!autoVerified) {
|
||||
const vToken = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?,?,?)').run(userId, vToken, expiresAt);
|
||||
|
||||
const appUrl = cfg.appUrl?.replace(/\/$/, '') || '';
|
||||
try {
|
||||
await sendMail({
|
||||
to: body.email,
|
||||
subject: `Bienvenue sur ${cfg.appName} — Vérifiez votre adresse email`,
|
||||
html: buildEmailHtml({
|
||||
title: `Bienvenue ${prenom || ''} !`,
|
||||
body: `<p>Votre compte a été créé avec succès. Pour commencer à utiliser ${cfg.appName}, veuillez confirmer votre adresse email en cliquant sur le bouton ci-dessous.</p>
|
||||
<p>Ce lien est valable <strong>24 heures</strong>.</p>`,
|
||||
ctaLabel: 'Vérifier mon adresse email',
|
||||
ctaUrl: `${appUrl}/verify-email?token=${vToken}`,
|
||||
}),
|
||||
});
|
||||
} catch (mailErr) {
|
||||
console.error('[auth] Échec envoi email bienvenue:', mailErr.message);
|
||||
// Ne pas faire échouer l'inscription si le mail échoue
|
||||
}
|
||||
|
||||
return res.status(201).json({ requiresVerification: true, email: body.email });
|
||||
}
|
||||
|
||||
const token = signToken({ sub: userId, email: body.email });
|
||||
res.status(201).json({
|
||||
token,
|
||||
user: { id: userId, email: body.email, displayName: body.displayName || null, role },
|
||||
user: { id: userId, email: body.email, displayName: body.displayName || null, role, email_verified: 1 },
|
||||
});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.post('/login', (req, res, next) => {
|
||||
const LoginSchema2FA = LoginSchema.extend({
|
||||
deviceToken: z.string().optional(),
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const body = LoginSchema.parse(req.body);
|
||||
const body = LoginSchema2FA.parse(req.body);
|
||||
const user = db
|
||||
.prepare('SELECT id, email, password_hash, display_name, role FROM users WHERE email = ?')
|
||||
.prepare('SELECT id, email, password_hash, display_name, role, email_verified, totp_enabled FROM users WHERE email = ?')
|
||||
.get(body.email);
|
||||
if (!user) throw new HttpError(401, 'Invalid credentials');
|
||||
|
||||
const ok = bcrypt.compareSync(body.password, user.password_hash);
|
||||
if (!ok) throw new HttpError(401, 'Invalid credentials');
|
||||
|
||||
if (!user.email_verified) {
|
||||
return res.status(403).json({
|
||||
error: 'Veuillez vérifier votre adresse email avant de vous connecter.',
|
||||
code: 'EMAIL_NOT_VERIFIED',
|
||||
email: user.email,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 2FA ──────────────────────────────────────────────────────────────
|
||||
if (user.totp_enabled) {
|
||||
// Vérifier si l'appareil est déjà de confiance
|
||||
if (body.deviceToken) {
|
||||
const dev = db.prepare(
|
||||
'SELECT id FROM two_fa_trusted_devices WHERE token = ? AND user_id = ? AND expires_at > datetime(\'now\')'
|
||||
).get(body.deviceToken, user.id);
|
||||
if (dev) {
|
||||
// Appareil de confiance — émettre le JWT directement
|
||||
const token = signToken({ sub: user.id, email: user.email });
|
||||
return res.json({
|
||||
token,
|
||||
user: { id: user.id, email: user.email, displayName: user.display_name, role: user.role, email_verified: 1, totp_enabled: 1 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Créer une session 2FA temporaire (5 minutes)
|
||||
const sessionToken = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO two_fa_sessions (user_id, token, expires_at) VALUES (?,?,?)').run(user.id, sessionToken, expiresAt);
|
||||
|
||||
return res.json({
|
||||
requires2FA: true,
|
||||
sessionToken,
|
||||
email: user.email,
|
||||
});
|
||||
}
|
||||
|
||||
const token = signToken({ sub: user.id, email: user.email });
|
||||
res.json({
|
||||
token,
|
||||
user: { id: user.id, email: user.email, displayName: user.display_name, role: user.role },
|
||||
user: { id: user.id, email: user.email, displayName: user.display_name, role: user.role, email_verified: 1, totp_enabled: 0 },
|
||||
});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
router.get('/me', requireAuth, (req, res) => {
|
||||
const user = db
|
||||
.prepare('SELECT id, email, display_name, role FROM users WHERE id = ?')
|
||||
.prepare('SELECT id, email, display_name, role, email_verified FROM users WHERE id = ?')
|
||||
.get(req.user.id);
|
||||
res.json({ user });
|
||||
});
|
||||
@@ -88,12 +163,12 @@ const UpdateMeSchema = z.object({
|
||||
newPassword: z.string().min(8).optional(),
|
||||
});
|
||||
|
||||
router.put('/me', requireAuth, (req, res, next) => {
|
||||
router.put('/me', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const body = UpdateMeSchema.parse(req.body);
|
||||
|
||||
const current = db
|
||||
.prepare('SELECT id, email, password_hash, display_name FROM users WHERE id = ?')
|
||||
.prepare('SELECT id, email, password_hash, display_name, email_verified FROM users WHERE id = ?')
|
||||
.get(req.user.id);
|
||||
|
||||
let newHash = undefined;
|
||||
@@ -114,18 +189,331 @@ router.put('/me', requireAuth, (req, res, next) => {
|
||||
const newEmail = body.email ?? current.email;
|
||||
const newDisplayName = body.displayName !== undefined ? body.displayName : current.display_name;
|
||||
const newPasswordHash = newHash ?? current.password_hash;
|
||||
const emailChanged = newEmail !== current.email;
|
||||
const newEmailVerified = emailChanged ? 0 : (current.email_verified ?? 1);
|
||||
|
||||
db.prepare(
|
||||
"UPDATE users SET email=?, display_name=?, password_hash=?, updated_at=datetime('now') WHERE id=?"
|
||||
).run(newEmail, newDisplayName, newPasswordHash, req.user.id);
|
||||
"UPDATE users SET email=?, display_name=?, password_hash=?, email_verified=?, updated_at=datetime('now') WHERE id=?"
|
||||
).run(newEmail, newDisplayName, newPasswordHash, newEmailVerified, req.user.id);
|
||||
|
||||
const token = newEmail !== current.email
|
||||
? signToken({ sub: req.user.id, email: newEmail })
|
||||
: undefined;
|
||||
// Si l'email change, envoyer un email de vérification sur la nouvelle adresse
|
||||
if (emailChanged) {
|
||||
const cfg = getSmtpConfig();
|
||||
if (cfg.enabled && cfg.host) {
|
||||
try {
|
||||
db.prepare('UPDATE email_verification_tokens SET used=1 WHERE user_id=? AND used=0').run(req.user.id);
|
||||
const vToken = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?,?,?)').run(req.user.id, vToken, expiresAt);
|
||||
const appUrl = cfg.appUrl?.replace(/\/$/, '') || '';
|
||||
await sendMail({
|
||||
to: newEmail,
|
||||
subject: `Vérifiez votre nouvelle adresse email — ${cfg.appName}`,
|
||||
html: buildEmailHtml({
|
||||
title: 'Vérification de votre nouvel email',
|
||||
body: `<p>Vous avez modifié votre adresse email. Cliquez ci-dessous pour confirmer cette nouvelle adresse.</p>`,
|
||||
ctaLabel: 'Vérifier mon adresse email',
|
||||
ctaUrl: `${appUrl}/verify-email?token=${vToken}`,
|
||||
}),
|
||||
});
|
||||
} catch (_) { /* Ne pas bloquer la réponse si SMTP échoue */ }
|
||||
}
|
||||
}
|
||||
|
||||
const newToken = emailChanged ? signToken({ sub: req.user.id, email: newEmail }) : undefined;
|
||||
const updatedUser = db.prepare('SELECT id, email, display_name, role, email_verified FROM users WHERE id=?').get(req.user.id);
|
||||
|
||||
res.json({
|
||||
user: { id: req.user.id, email: newEmail, display_name: newDisplayName },
|
||||
...(token ? { token } : {}),
|
||||
user: updatedUser,
|
||||
...(newToken ? { token: newToken } : {}),
|
||||
...(emailChanged ? { requiresVerification: true } : {}),
|
||||
});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── Vérification d'adresse email ──────────────────────────────────────────
|
||||
router.get('/verify-email', (req, res, next) => {
|
||||
try {
|
||||
const token = req.query.token;
|
||||
if (!token) throw new HttpError(400, 'Token manquant.');
|
||||
|
||||
const row = db.prepare(
|
||||
`SELECT evt.*, u.id AS uid FROM email_verification_tokens evt
|
||||
JOIN users u ON u.id = evt.user_id
|
||||
WHERE evt.token = ? AND evt.used = 0`
|
||||
).get(token);
|
||||
|
||||
if (!row) throw new HttpError(400, 'Lien invalide ou déjà utilisé.');
|
||||
if (new Date(row.expires_at) < new Date()) throw new HttpError(400, 'Ce lien a expiré. Demandez un nouvel email de vérification.');
|
||||
|
||||
db.prepare("UPDATE users SET email_verified=1, updated_at=datetime('now') WHERE id=?").run(row.uid);
|
||||
db.prepare('UPDATE email_verification_tokens SET used=1 WHERE id=?').run(row.id);
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── Renvoi de l'email de vérification ─────────────────────────────────────
|
||||
router.post('/resend-verification', async (req, res, next) => {
|
||||
try {
|
||||
const { email } = z.object({ email: z.string().email() }).parse(req.body);
|
||||
const user = db.prepare('SELECT id, email, display_name, email_verified FROM users WHERE email=?').get(email);
|
||||
|
||||
// Répondre OK même si l'email n'existe pas
|
||||
if (!user || user.email_verified) return res.json({ ok: true });
|
||||
|
||||
const cfg = getSmtpConfig();
|
||||
if (!cfg.enabled) throw new HttpError(503, "SMTP désactivé — impossible d'envoyer l'email.");
|
||||
|
||||
// Invalider les anciens tokens
|
||||
db.prepare('UPDATE email_verification_tokens SET used=1 WHERE user_id=? AND used=0').run(user.id);
|
||||
|
||||
const vToken = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?,?,?)').run(user.id, vToken, expiresAt);
|
||||
|
||||
const appUrl = cfg.appUrl?.replace(/\/$/, '') || '';
|
||||
const prenom = (user.display_name || user.email).split(' ')[0];
|
||||
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: `Vérifiez votre adresse email — ${cfg.appName}`,
|
||||
html: buildEmailHtml({
|
||||
title: 'Confirmez votre adresse email',
|
||||
body: `<p>Bonjour ${prenom},</p>
|
||||
<p>Cliquez sur le bouton ci-dessous pour vérifier votre adresse email. Ce lien est valable <strong>24 heures</strong>.</p>`,
|
||||
ctaLabel: 'Vérifier mon adresse email',
|
||||
ctaUrl: `${appUrl}/verify-email?token=${vToken}`,
|
||||
}),
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── Mot de passe oublié ───────────────────────────────────────────────────
|
||||
router.post('/forgot-password', async (req, res, next) => {
|
||||
try {
|
||||
const { email } = z.object({ email: z.string().email() }).parse(req.body);
|
||||
const user = db.prepare('SELECT id, email, display_name FROM users WHERE email = ?').get(email);
|
||||
|
||||
// Toujours répondre OK pour ne pas divulguer si l'email existe
|
||||
if (!user) return res.json({ ok: true });
|
||||
|
||||
const cfg = getSmtpConfig();
|
||||
if (!cfg.enabled) throw new HttpError(503, 'La réinitialisation par email n\'est pas disponible (SMTP désactivé).');
|
||||
|
||||
// Invalider les anciens tokens non utilisés
|
||||
db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE user_id = ? AND used = 0').run(user.id);
|
||||
|
||||
// Générer un token sécurisé (1h de validité)
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)').run(user.id, token, expiresAt);
|
||||
|
||||
const appUrl = cfg.appUrl?.replace(/\/$/, '') || '';
|
||||
const resetUrl = `${appUrl}/reset-password?token=${token}`;
|
||||
const prenom = (user.display_name || user.email).split(' ')[0];
|
||||
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: `Réinitialisation de votre mot de passe — ${cfg.appName}`,
|
||||
html: buildEmailHtml({
|
||||
title: 'Réinitialisation du mot de passe',
|
||||
body: `<p>Bonjour ${prenom},</p>
|
||||
<p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour en définir un nouveau. Ce lien est valable <strong>1 heure</strong>.</p>
|
||||
<p>Si vous n'êtes pas à l'origine de cette demande, ignorez simplement cet email.</p>`,
|
||||
ctaLabel: 'Réinitialiser mon mot de passe',
|
||||
ctaUrl: resetUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ── Réinitialisation du mot de passe ──────────────────────────────────────
|
||||
router.post('/reset-password', async (req, res, next) => {
|
||||
try {
|
||||
const { token, password } = z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8),
|
||||
}).parse(req.body);
|
||||
|
||||
const row = db.prepare(
|
||||
`SELECT prt.*, u.id AS uid FROM password_reset_tokens prt
|
||||
JOIN users u ON u.id = prt.user_id
|
||||
WHERE prt.token = ? AND prt.used = 0`
|
||||
).get(token);
|
||||
|
||||
if (!row) throw new HttpError(400, 'Lien invalide ou déjà utilisé.');
|
||||
if (new Date(row.expires_at) < new Date()) throw new HttpError(400, 'Ce lien a expiré. Faites une nouvelle demande.');
|
||||
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
db.prepare("UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?").run(hash, row.uid);
|
||||
db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE id = ?').run(row.id);
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// ── 2FA — Configuration TOTP ─────────────────────────────────────────────
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// GET /2fa/setup — génère un secret TOTP + QR code pour l'utilisateur
|
||||
router.get('/2fa/setup', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { authenticator } = await import('otplib');
|
||||
const QRCode = (await import('qrcode')).default;
|
||||
|
||||
const cfg = getSmtpConfig();
|
||||
const issuer = cfg.appName || 'Crowdlending Tracker';
|
||||
const user = db.prepare('SELECT id, email, totp_secret, totp_enabled FROM users WHERE id=?').get(req.user.id);
|
||||
|
||||
// Générer un nouveau secret (ou réutiliser si setup pas encore confirmé)
|
||||
const secret = (user.totp_enabled ? null : user.totp_secret) || authenticator.generateSecret();
|
||||
|
||||
if (!user.totp_enabled) {
|
||||
db.prepare("UPDATE users SET totp_secret=? WHERE id=?").run(secret, req.user.id);
|
||||
}
|
||||
|
||||
const uri = authenticator.keyuri(user.email, issuer, secret);
|
||||
const qrCode = await QRCode.toDataURL(uri);
|
||||
|
||||
res.json({ secret, qrCode, issuer, email: user.email, totp_enabled: !!user.totp_enabled });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// POST /2fa/confirm-setup — vérifie le code TOTP et active le 2FA
|
||||
router.post('/2fa/confirm-setup', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { code } = z.object({ code: z.string().length(6) }).parse(req.body);
|
||||
const { authenticator } = await import('otplib');
|
||||
|
||||
const user = db.prepare('SELECT totp_secret, totp_enabled FROM users WHERE id=?').get(req.user.id);
|
||||
if (!user.totp_secret) throw new HttpError(400, 'Lancez d\'abord la configuration 2FA.');
|
||||
if (user.totp_enabled) throw new HttpError(400, 'Le 2FA est déjà activé.');
|
||||
|
||||
authenticator.options = { window: 1 };
|
||||
const valid = authenticator.verify({ token: code, secret: user.totp_secret });
|
||||
if (!valid) throw new HttpError(400, 'Code invalide. Réessayez.');
|
||||
|
||||
db.prepare("UPDATE users SET totp_enabled=1 WHERE id=?").run(req.user.id);
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// POST /2fa/disable — désactive le 2FA (mot de passe requis)
|
||||
router.post('/2fa/disable', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { password } = z.object({ password: z.string().min(1) }).parse(req.body);
|
||||
|
||||
const user = db.prepare('SELECT password_hash, totp_enabled FROM users WHERE id=?').get(req.user.id);
|
||||
if (!user.totp_enabled) throw new HttpError(400, 'Le 2FA n\'est pas activé.');
|
||||
|
||||
const ok = bcrypt.compareSync(password, user.password_hash);
|
||||
if (!ok) throw new HttpError(401, 'Mot de passe incorrect.');
|
||||
|
||||
db.prepare("UPDATE users SET totp_enabled=0, totp_secret=NULL WHERE id=?").run(req.user.id);
|
||||
// Supprimer tous les appareils de confiance
|
||||
db.prepare('DELETE FROM two_fa_trusted_devices WHERE user_id=?').run(req.user.id);
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// ── 2FA — Vérification lors de la connexion ──────────────────────────────
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// POST /2fa/send-email-code — envoie un OTP par email (flow 2FA)
|
||||
router.post('/2fa/send-email-code', async (req, res, next) => {
|
||||
try {
|
||||
const { sessionToken } = z.object({ sessionToken: z.string().min(1) }).parse(req.body);
|
||||
|
||||
const sess = db.prepare(
|
||||
"SELECT * FROM two_fa_sessions WHERE token=? AND used=0 AND expires_at > datetime('now')"
|
||||
).get(sessionToken);
|
||||
if (!sess) throw new HttpError(401, 'Session expirée. Reconnectez-vous.');
|
||||
|
||||
const user = db.prepare('SELECT id, email, display_name FROM users WHERE id=?').get(sess.user_id);
|
||||
|
||||
// Invalider les anciens codes
|
||||
db.prepare('UPDATE two_fa_email_codes SET used=1 WHERE user_id=? AND used=0').run(user.id);
|
||||
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO two_fa_email_codes (user_id, code, expires_at) VALUES (?,?,?)').run(user.id, code, expiresAt);
|
||||
|
||||
const cfg = getSmtpConfig();
|
||||
if (!cfg.enabled) throw new HttpError(503, "SMTP désactivé — impossible d'envoyer le code.");
|
||||
|
||||
const prenom = (user.display_name || user.email).split(' ')[0];
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: `${code} — Votre code de connexion`,
|
||||
html: buildEmailHtml({
|
||||
title: 'Code de vérification',
|
||||
body: `<p>Bonjour ${prenom},</p>
|
||||
<p>Voici votre code de vérification à usage unique :</p>
|
||||
<p style="font-size:32px;font-weight:700;letter-spacing:8px;text-align:center;margin:24px 0;">${code}</p>
|
||||
<p>Ce code est valable <strong>5 minutes</strong>. Ne le communiquez à personne.</p>`,
|
||||
}),
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// POST /2fa/verify — vérifie le code (TOTP ou email) et émet le JWT final
|
||||
router.post('/2fa/verify', async (req, res, next) => {
|
||||
try {
|
||||
const { sessionToken, code, method, trustDevice } = z.object({
|
||||
sessionToken: z.string().min(1),
|
||||
code: z.string().min(6).max(6),
|
||||
method: z.enum(['totp', 'email']),
|
||||
trustDevice: z.boolean().optional(),
|
||||
}).parse(req.body);
|
||||
|
||||
const sess = db.prepare(
|
||||
"SELECT * FROM two_fa_sessions WHERE token=? AND used=0 AND expires_at > datetime('now')"
|
||||
).get(sessionToken);
|
||||
if (!sess) throw new HttpError(401, 'Session expirée. Reconnectez-vous.');
|
||||
|
||||
const user = db.prepare('SELECT id, email, display_name, role, totp_secret, totp_enabled FROM users WHERE id=?').get(sess.user_id);
|
||||
|
||||
if (method === 'totp') {
|
||||
const { authenticator } = await import('otplib');
|
||||
authenticator.options = { window: 1 };
|
||||
const valid = authenticator.verify({ token: code, secret: user.totp_secret });
|
||||
if (!valid) throw new HttpError(400, 'Code invalide.');
|
||||
} else {
|
||||
// Email OTP
|
||||
const row = db.prepare(
|
||||
"SELECT id FROM two_fa_email_codes WHERE user_id=? AND code=? AND used=0 AND expires_at > datetime('now')"
|
||||
).get(user.id, code);
|
||||
if (!row) throw new HttpError(400, 'Code invalide ou expiré.');
|
||||
db.prepare('UPDATE two_fa_email_codes SET used=1 WHERE id=?').run(row.id);
|
||||
}
|
||||
|
||||
// Invalider la session 2FA
|
||||
db.prepare('UPDATE two_fa_sessions SET used=1 WHERE id=?').run(sess.id);
|
||||
|
||||
// Appareil de confiance (30 jours)
|
||||
let deviceToken = null;
|
||||
if (trustDevice) {
|
||||
deviceToken = crypto.randomBytes(32).toString('hex');
|
||||
const devExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare('INSERT INTO two_fa_trusted_devices (user_id, token, expires_at) VALUES (?,?,?)').run(user.id, deviceToken, devExpires);
|
||||
}
|
||||
|
||||
const token = signToken({ sub: user.id, email: user.email });
|
||||
res.json({
|
||||
token,
|
||||
user: { id: user.id, email: user.email, displayName: user.display_name, role: user.role, email_verified: 1, totp_enabled: 1 },
|
||||
...(deviceToken ? { deviceToken } : {}),
|
||||
});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
@@ -41,6 +41,8 @@ import refSecteursRouter from './routes/ref-secteurs.js';
|
||||
import categoriesInvRouter from './routes/categories-inv.js';
|
||||
import secteursInvRouter from './routes/secteurs-inv.js';
|
||||
import associationsInvRouter from './routes/associations-inv.js';
|
||||
import db from './db/index.js';
|
||||
import { getSmtpConfig } from './utils/mailer.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 4000;
|
||||
@@ -69,6 +71,20 @@ const authLimiter = rateLimit({
|
||||
|
||||
app.get('/api/health', (_, res) => res.json({ ok: true, ts: new Date().toISOString() }));
|
||||
|
||||
// Informations publiques de la plateforme (utilisées sur la page de login)
|
||||
app.get('/api/app-info', (_, res) => {
|
||||
try {
|
||||
const cfg = getSmtpConfig();
|
||||
const icon = db.prepare(`SELECT filename FROM app_icons WHERE name = 'logo-app' LIMIT 1`).get();
|
||||
res.json({
|
||||
appName: cfg.appName || 'Crowdlending Tracker',
|
||||
iconUrl: icon ? `/api/icons-files/${icon.filename}` : null,
|
||||
});
|
||||
} catch {
|
||||
res.json({ appName: 'Crowdlending Tracker', iconUrl: null });
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/auth', authLimiter, authRouter);
|
||||
|
||||
// All routes below require authentication
|
||||
|
||||
@@ -25,7 +25,7 @@ export function getSmtpConfig() {
|
||||
username: row?.username || process.env.SMTP_USERNAME || '',
|
||||
password: row?.password || process.env.SMTP_PASSWORD || '',
|
||||
allowUnauth: !!(row?.allow_unauth),
|
||||
appName: row?.app_name || process.env.APP_NAME || 'Crowdlending',
|
||||
appName: row?.app_name || process.env.APP_NAME || 'Crowdlending Tracker',
|
||||
appUrl: row?.app_url || process.env.APP_URL || '',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user