fix: QR token auto-refresh on patient join + default rotation 24h
This commit is contained in:
parent
dc5cf250be
commit
34126dc5ae
11 changed files with 578 additions and 36 deletions
360
package-lock.json
generated
360
package-lock.json
generated
|
|
@ -53,6 +53,7 @@
|
|||
"nodemailer": "^8.0.6",
|
||||
"p-queue": "^9.1.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -81,9 +82,11 @@
|
|||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^9.1.1",
|
||||
"drizzle-kit": "^0.30.1",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.0",
|
||||
|
|
@ -3190,6 +3193,29 @@
|
|||
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@paralleldrive/cuid2": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@petamoriken/float16": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz",
|
||||
|
|
@ -5353,6 +5379,13 @@
|
|||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
|
|
@ -5481,6 +5514,13 @@
|
|||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
|
@ -5596,6 +5636,30 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/superagent": {
|
||||
"version": "8.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
|
||||
"integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookiejar": "^2.1.5",
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/supertest": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz",
|
||||
"integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
|
|
@ -5941,6 +6005,13 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
|
|
@ -5975,6 +6046,13 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
|
|
@ -6438,6 +6516,25 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
|
|
@ -6453,6 +6550,16 @@
|
|||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
|
|
@ -6533,6 +6640,13 @@
|
|||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.49.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
|
||||
|
|
@ -6801,6 +6915,15 @@
|
|||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
|
@ -6886,6 +7009,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
|
|
@ -6929,6 +7062,17 @@
|
|||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
|
|
@ -7152,6 +7296,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
|
||||
|
|
@ -7548,6 +7701,12 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz",
|
||||
"integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -7569,6 +7728,12 @@
|
|||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-safe-stringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
|
|
@ -7733,6 +7898,41 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dezalgo": "^1.0.4",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
|
|
@ -8164,6 +8364,12 @@
|
|||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hookified": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
|
||||
|
|
@ -8785,6 +8991,15 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -9447,6 +9662,15 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
|
|
@ -9686,6 +9910,15 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
|
|
@ -9909,6 +10142,30 @@
|
|||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
|
||||
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.7",
|
||||
"dateformat": "^4.6.3",
|
||||
"fast-copy": "^4.0.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"help-me": "^5.0.0",
|
||||
"joycon": "^3.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"secure-json-parse": "^4.0.0",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"strip-json-comments": "^5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"pino-pretty": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
|
|
@ -10061,6 +10318,16 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -10764,6 +11031,22 @@
|
|||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
|
@ -11394,6 +11677,18 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "22.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.0.tgz",
|
||||
|
|
@ -11427,6 +11722,65 @@
|
|||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
|
||||
"integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"component-emitter": "^1.3.1",
|
||||
"cookiejar": "^2.1.4",
|
||||
"debug": "^4.3.7",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.5",
|
||||
"formidable": "^3.5.4",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent/node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supertest": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
|
||||
"integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie-signature": "^1.2.2",
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^10.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supertest/node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
|
|
@ -14601,6 +14955,12 @@
|
|||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"nodemailer": "^8.0.6",
|
||||
"p-queue": "^9.1.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -89,9 +90,11 @@
|
|||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^9.1.1",
|
||||
"drizzle-kit": "^0.30.1",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -12,19 +12,23 @@ import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|||
import { appRouter } from "../routers.js";
|
||||
import { createContext } from "./context.js";
|
||||
import { authMiddleware, assertAuthEnv } from "../auth.js";
|
||||
import { getDb } from "../db.js";
|
||||
import { getDb, pingDb } from "../db.js";
|
||||
import { startAutoAbsentJob, stopAutoAbsentJob } from "../services/autoAbsent.js";
|
||||
import {
|
||||
handleWebhook as handleStripeWebhook,
|
||||
isStripeConfigured,
|
||||
verifyAndConstructEvent,
|
||||
} from "../services/stripe.js";
|
||||
import { getActiveWhatsAppSessionsCount } from "../services/whatsapp.js";
|
||||
import { logger, childLogger } from "./logger.js";
|
||||
import { requestLogger } from "./requestLogger.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const PORT = Number(process.env.PORT ?? 5000);
|
||||
const NODE_ENV = process.env.NODE_ENV ?? "development";
|
||||
const IS_PROD = NODE_ENV === "production";
|
||||
const BACKUP_DIR = process.env.BACKUP_DIR ?? "/app/data/backups";
|
||||
|
||||
const PROD_ORIGINS = ["https://attente.cosmolan.fr"];
|
||||
const DEV_ORIGINS = [
|
||||
|
|
@ -35,6 +39,39 @@ const DEV_ORIGINS = [
|
|||
];
|
||||
const ALLOWED_ORIGINS = IS_PROD ? PROD_ORIGINS : DEV_ORIGINS;
|
||||
|
||||
const serverLog = childLogger("server");
|
||||
const dbLog = childLogger("db");
|
||||
const stripeLog = childLogger("stripe");
|
||||
const trpcLog = childLogger("trpc");
|
||||
|
||||
function readPackageVersion(): string {
|
||||
try {
|
||||
const pkgPath = path.resolve(ROOT, "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { version?: string };
|
||||
return pkg.version ?? "0.0.0";
|
||||
} catch {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
const APP_VERSION = readPackageVersion();
|
||||
|
||||
function getLastBackupTimestamp(): string | null {
|
||||
try {
|
||||
if (!fs.existsSync(BACKUP_DIR)) return null;
|
||||
const files = fs.readdirSync(BACKUP_DIR);
|
||||
let mostRecent = 0;
|
||||
for (const file of files) {
|
||||
const stat = fs.statSync(path.join(BACKUP_DIR, file));
|
||||
if (stat.isFile() && stat.mtimeMs > mostRecent) {
|
||||
mostRecent = stat.mtimeMs;
|
||||
}
|
||||
}
|
||||
return mostRecent > 0 ? new Date(mostRecent).toISOString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
// Fail fast if critical secrets are missing — refuse to start instead of
|
||||
// erroring lazily on the first authenticated request.
|
||||
|
|
@ -43,11 +80,9 @@ async function bootstrap() {
|
|||
// Eagerly initialize database connection (warns early if DATABASE_URL missing)
|
||||
try {
|
||||
await getDb();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[db] connected");
|
||||
dbLog.info("connected");
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[db] connection failed:", err);
|
||||
dbLog.error({ err }, "connection failed");
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
|
@ -88,6 +123,9 @@ async function bootstrap() {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Request logging (must come early to capture every API request) ───────
|
||||
app.use(requestLogger);
|
||||
|
||||
// ── Security headers ─────────────────────────────────────────────────────
|
||||
// CSP is intentionally disabled here: it conflicts with the Vite dev server
|
||||
// and the inline assets generated by the SPA build. Re-enable once a proper
|
||||
|
|
@ -181,8 +219,7 @@ async function bootstrap() {
|
|||
await handleStripeWebhook(event);
|
||||
res.status(200).json({ received: true });
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[stripe] webhook error:", err);
|
||||
stripeLog.error({ err }, "webhook error");
|
||||
const message = err instanceof Error ? err.message : "Webhook error";
|
||||
res.status(400).json({ error: message });
|
||||
}
|
||||
|
|
@ -194,9 +231,38 @@ async function bootstrap() {
|
|||
app.use(cookieParser());
|
||||
app.use(authMiddleware);
|
||||
|
||||
// ── Health check ─────────────────────────────────────────────────────────
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({ status: "ok", env: NODE_ENV, ts: Date.now() });
|
||||
// ── Health / readiness / liveness probes ─────────────────────────────────
|
||||
app.get("/api/health", async (_req, res) => {
|
||||
const dbStatus = await pingDb();
|
||||
const lastBackup = getLastBackupTimestamp();
|
||||
res.json({
|
||||
status: dbStatus.ok ? "ok" : "degraded",
|
||||
env: NODE_ENV,
|
||||
version: APP_VERSION,
|
||||
uptime: process.uptime(),
|
||||
ts: Date.now(),
|
||||
database: dbStatus.ok
|
||||
? { status: "connected", latencyMs: dbStatus.latencyMs }
|
||||
: { status: "error", error: dbStatus.error },
|
||||
whatsapp: {
|
||||
activeSessions: getActiveWhatsAppSessionsCount(),
|
||||
},
|
||||
lastBackup,
|
||||
});
|
||||
});
|
||||
|
||||
// k8s-style probes
|
||||
app.get("/api/live", (_req, res) => {
|
||||
res.json({ status: "ok", ts: Date.now() });
|
||||
});
|
||||
|
||||
app.get("/api/ready", async (_req, res) => {
|
||||
const dbStatus = await pingDb();
|
||||
if (!dbStatus.ok) {
|
||||
res.status(503).json({ status: "not_ready", database: "error", error: dbStatus.error });
|
||||
return;
|
||||
}
|
||||
res.json({ status: "ready", database: "connected", latencyMs: dbStatus.latencyMs });
|
||||
});
|
||||
|
||||
// ── tRPC ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -207,8 +273,9 @@ async function bootstrap() {
|
|||
createContext,
|
||||
onError({ error, path }) {
|
||||
if (error.code === "INTERNAL_SERVER_ERROR") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[trpc] ${path}:`, error);
|
||||
trpcLog.error({ err: error, path }, "internal server error");
|
||||
} else {
|
||||
trpcLog.debug({ code: error.code, path, message: error.message }, "trpc error");
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -227,28 +294,24 @@ async function bootstrap() {
|
|||
res.sendFile(indexHtml);
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[static] dist/client not found at ${clientDist}`);
|
||||
serverLog.warn({ clientDist }, "static dist/client not found");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error handler ────────────────────────────────────────────────────────
|
||||
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[express] error:", err);
|
||||
serverLog.error({ err }, "express error");
|
||||
res.status(500).json({ error: "Internal Server Error" });
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[server] listening on http://0.0.0.0:${PORT} (${NODE_ENV})`);
|
||||
serverLog.info({ port: PORT, env: NODE_ENV, version: APP_VERSION }, "server listening");
|
||||
// Démarre le job qui marque les patients absents après N minutes sans réponse
|
||||
startAutoAbsentJob();
|
||||
});
|
||||
|
||||
const shutdown = (signal: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[server] received ${signal}, shutting down`);
|
||||
serverLog.info({ signal }, "received shutdown signal");
|
||||
stopAutoAbsentJob();
|
||||
io.close();
|
||||
httpServer.close(() => process.exit(0));
|
||||
|
|
@ -259,7 +322,6 @@ async function bootstrap() {
|
|||
}
|
||||
|
||||
bootstrap().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[server] failed to start:", err);
|
||||
logger.fatal({ err }, "server failed to start");
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
44
server/_core/logger.ts
Normal file
44
server/_core/logger.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Structured logger built on pino.
|
||||
*
|
||||
* - level: from LOG_LEVEL env var (default "info")
|
||||
* - dev: pretty-printed, colorized output
|
||||
* - prod: JSON output suitable for ingestion by log shippers
|
||||
*
|
||||
* Use child loggers (`logger.child({ component: "name" })`) to tag log lines
|
||||
* by subsystem so they remain easy to grep in production.
|
||||
*/
|
||||
import pino, { type LoggerOptions } from "pino";
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV ?? "development";
|
||||
const IS_PROD = NODE_ENV === "production";
|
||||
const LEVEL = process.env.LOG_LEVEL ?? "info";
|
||||
|
||||
const baseOptions: LoggerOptions = {
|
||||
level: LEVEL,
|
||||
base: { env: NODE_ENV },
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
};
|
||||
|
||||
const transport = IS_PROD
|
||||
? undefined
|
||||
: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: "SYS:HH:MM:ss.l",
|
||||
ignore: "pid,hostname,env",
|
||||
singleLine: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const logger = pino({
|
||||
...baseOptions,
|
||||
...(transport ? { transport } : {}),
|
||||
});
|
||||
|
||||
export type Logger = typeof logger;
|
||||
|
||||
export function childLogger(component: string, extra: Record<string, unknown> = {}) {
|
||||
return logger.child({ component, ...extra });
|
||||
}
|
||||
32
server/_core/requestLogger.ts
Normal file
32
server/_core/requestLogger.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Lightweight HTTP request logger that records method, url, status and duration
|
||||
* for every incoming request. Skips noisy probe endpoints to keep logs clean.
|
||||
*/
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { childLogger } from "./logger.js";
|
||||
|
||||
const httpLog = childLogger("http");
|
||||
|
||||
const SKIP_PATHS = new Set(["/api/live", "/api/ready", "/api/health"]);
|
||||
|
||||
export function requestLogger(req: Request, res: Response, next: NextFunction): void {
|
||||
if (SKIP_PATHS.has(req.path)) return next();
|
||||
|
||||
const startNs = process.hrtime.bigint();
|
||||
|
||||
res.on("finish", () => {
|
||||
const durationMs = Number((process.hrtime.bigint() - startNs) / 1_000_000n);
|
||||
const status = res.statusCode;
|
||||
const payload = {
|
||||
method: req.method,
|
||||
url: req.originalUrl ?? req.url,
|
||||
status,
|
||||
durationMs,
|
||||
};
|
||||
if (status >= 500) httpLog.error(payload, "request failed");
|
||||
else if (status >= 400) httpLog.warn(payload, "request error");
|
||||
else httpLog.info(payload, "request ok");
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
20
server/db.ts
20
server/db.ts
|
|
@ -2,6 +2,7 @@ import { drizzle, type MySql2Database } from "drizzle-orm/mysql2";
|
|||
import mysql from "mysql2/promise";
|
||||
import { and, asc, desc, eq, gte, inArray, like, lt, or, sql } from "drizzle-orm";
|
||||
import crypto from "node:crypto";
|
||||
import { childLogger } from "./_core/logger.js";
|
||||
import {
|
||||
users,
|
||||
subscriptions,
|
||||
|
|
@ -69,6 +70,22 @@ export async function closeDb() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping the database with a trivial query. Used by health/readiness probes.
|
||||
* Returns the latency in ms on success, or an error message on failure.
|
||||
*/
|
||||
export async function pingDb(): Promise<{ ok: true; latencyMs: number } | { ok: false; error: string }> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const db = await getDb();
|
||||
await db.execute(sql`SELECT 1`);
|
||||
return { ok: true, latencyMs: Date.now() - start };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Users ───────────────────────────────────────────────────────────────────
|
||||
export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
const db = await getDb();
|
||||
|
|
@ -543,8 +560,7 @@ export async function insertWhatsAppLog(
|
|||
try {
|
||||
await db.insert(whatsappLogs).values(data);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[WhatsApp Log] Failed to insert log:", err);
|
||||
childLogger("whatsapp-log").warn({ err }, "failed to insert log");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ import {
|
|||
isStripeConfigured,
|
||||
} from "./services/stripe.js";
|
||||
import { checkPlanLimit, getPlanLimitsForUser } from "./services/planLimits.js";
|
||||
import { childLogger } from "./_core/logger.js";
|
||||
|
||||
const authLog = childLogger("auth");
|
||||
|
||||
// ─── Socket.io accessor ──────────────────────────────────────────────────────
|
||||
function getIo(): SocketIOServer | null {
|
||||
|
|
@ -250,7 +253,7 @@ const authRouter = router({
|
|||
try {
|
||||
await sendMail({ to: user.email, subject, html, text });
|
||||
} catch (err) {
|
||||
console.error("[auth.forgotPassword] sendMail failed", err);
|
||||
authLog.error({ err, userId: user.id }, "forgotPassword sendMail failed");
|
||||
}
|
||||
}
|
||||
return { success: true };
|
||||
|
|
@ -689,7 +692,9 @@ const queueRouter = router({
|
|||
clinic.qrRotationMinutes &&
|
||||
clinic.qrRotationMinutes > 0
|
||||
) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "QR code expiré" });
|
||||
// Auto-refresh expired QR token so patients can always join
|
||||
const fresh = await rotateQrToken(clinic.id);
|
||||
clinic = (await getClinicById(clinic.id)) ?? clinic;
|
||||
}
|
||||
if (!clinic.isQueueOpen) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "La file est fermée" });
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const clinics = mysqlTable(
|
|||
// QR code token rotatif anti-triche
|
||||
qrToken: varchar("qrToken", { length: 64 }).notNull(),
|
||||
qrTokenExpiresAt: timestamp("qrTokenExpiresAt"),
|
||||
qrRotationMinutes: int("qrRotationMinutes").default(30),
|
||||
qrRotationMinutes: int("qrRotationMinutes").default(1440),
|
||||
// Paramètres file d'attente
|
||||
avgConsultationMinutes: int("avgConsultationMinutes").default(15),
|
||||
maxQueueSize: int("maxQueueSize").default(50),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
import { getDb } from "../db.js";
|
||||
import { clinics, queueEntries } from "../schema.js";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { childLogger } from "../_core/logger.js";
|
||||
|
||||
const log = childLogger("auto-absent");
|
||||
// Socket.io helpers (utilise le global injecté par le serveur principal)
|
||||
function emitToClinic(clinicId: number, event: string, data: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -25,7 +28,7 @@ let intervalId: ReturnType<typeof setInterval> | null = null;
|
|||
export function startAutoAbsentJob(): void {
|
||||
if (intervalId) return; // already running
|
||||
intervalId = setInterval(checkAutoAbsent, 30_000);
|
||||
console.log("[AutoAbsent] Job démarré (intervalle 30s)");
|
||||
log.info("job started (interval 30s)");
|
||||
}
|
||||
|
||||
/** Arrête le job */
|
||||
|
|
@ -33,7 +36,7 @@ export function stopAutoAbsentJob(): void {
|
|||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
console.log("[AutoAbsent] Job arrêté");
|
||||
log.info("job stopped");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +90,13 @@ async function checkAutoAbsent(): Promise<void> {
|
|||
.set({ status: "absent" })
|
||||
.where(eq(queueEntries.id, entry.id));
|
||||
|
||||
console.log(
|
||||
`[AutoAbsent] Cabinet ${clinic.id} — ticket #${entry.ticketNumber} marqué absent après ${Math.round(elapsed / 60000)} min`
|
||||
log.info(
|
||||
{
|
||||
clinicId: clinic.id,
|
||||
ticketNumber: entry.ticketNumber,
|
||||
elapsedMinutes: Math.round(elapsed / 60000),
|
||||
},
|
||||
"patient marked absent"
|
||||
);
|
||||
|
||||
// Notifier le patient (s'il est encore connecté)
|
||||
|
|
@ -105,7 +113,7 @@ async function checkAutoAbsent(): Promise<void> {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[AutoAbsent] Erreur lors de la vérification :", err);
|
||||
log.error({ err }, "check failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import nodemailer, { type Transporter } from "nodemailer";
|
||||
import { childLogger } from "../_core/logger.js";
|
||||
|
||||
const log = childLogger("email");
|
||||
|
||||
let cachedTransporter: Transporter | null = null;
|
||||
|
||||
|
|
@ -9,7 +12,7 @@ function getTransporter(): Transporter | null {
|
|||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
if (!host || !port) {
|
||||
console.warn("[email] SMTP_HOST/SMTP_PORT not configured — emails will be logged only");
|
||||
log.warn("SMTP_HOST/SMTP_PORT not configured — emails will be logged only");
|
||||
return null;
|
||||
}
|
||||
cachedTransporter = nodemailer.createTransport({
|
||||
|
|
@ -25,7 +28,7 @@ export async function sendMail(opts: { to: string; subject: string; html: string
|
|||
const from = process.env.SMTP_FROM ?? process.env.SMTP_USER ?? "no-reply@queuemed.app";
|
||||
const transporter = getTransporter();
|
||||
if (!transporter) {
|
||||
console.info("[email] (dev) Would send email", { to: opts.to, subject: opts.subject });
|
||||
log.info({ to: opts.to, subject: opts.subject }, "(dev) would send email");
|
||||
return;
|
||||
}
|
||||
await transporter.sendMail({
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import pino from "pino";
|
|||
import { EventEmitter } from "events";
|
||||
import PQueue from "p-queue";
|
||||
import { insertWhatsAppLog, maskPhone } from "../db.js";
|
||||
import { childLogger } from "../_core/logger.js";
|
||||
|
||||
const waLog = childLogger("whatsapp");
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
export type WAStatus = "disconnected" | "connecting" | "qr_ready" | "connected";
|
||||
|
|
@ -57,8 +60,7 @@ const SESSION_DIR = process.env.WHATSAPP_SESSION_DIR ?? "/app/data/whatsapp-sess
|
|||
try {
|
||||
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[whatsapp] failed to create session dir at ${SESSION_DIR}:`, err);
|
||||
waLog.error({ err, sessionDir: SESSION_DIR }, "failed to create session dir");
|
||||
}
|
||||
|
||||
// Silent logger to avoid spamming server logs
|
||||
|
|
@ -136,6 +138,7 @@ export async function connectWhatsApp(clinicId: number): Promise<void> {
|
|||
} catch {
|
||||
session.qrCode = null;
|
||||
}
|
||||
waLog.info({ clinicId, event: "qr_ready" }, "QR code generated");
|
||||
session.events.emit("qr", { qrCode: session.qrCode, qrRaw: qr });
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +147,7 @@ export async function connectWhatsApp(clinicId: number): Promise<void> {
|
|||
session.qrCode = null;
|
||||
session.qrRaw = null;
|
||||
session.retryCount = 0;
|
||||
waLog.info({ clinicId, event: "connected" }, "session connected");
|
||||
session.events.emit("connected");
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +158,11 @@ export async function connectWhatsApp(clinicId: number): Promise<void> {
|
|||
session.status = "disconnected";
|
||||
session.socket = null;
|
||||
|
||||
waLog.warn(
|
||||
{ clinicId, event: "disconnected", statusCode, shouldReconnect, retryCount: session.retryCount },
|
||||
"session disconnected"
|
||||
);
|
||||
|
||||
if (shouldReconnect && session.retryCount < 5) {
|
||||
session.retryCount++;
|
||||
setTimeout(() => connectWhatsApp(clinicId), 3000 * session.retryCount);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue