Compare commits
No commits in common. "a9c4b29f9f2d6d7fc0fecd4ca2a42014eefbefff" and "a2d62ddec1dd2da318d899bb679803ab7f258e64" have entirely different histories.
a9c4b29f9f
...
a2d62ddec1
30
config.json
30
config.json
@ -46,16 +46,6 @@
|
|||||||
"filesBucket": "farmcontrol"
|
"filesBucket": "farmcontrol"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smtp": {
|
|
||||||
"host": "mail.tombutcher.work",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"auth": {
|
|
||||||
"user": "farmcontrol",
|
|
||||||
"pass": "XwV5u3jWufuo5E5U4N9hBHfNfwk28D7fNdFN"
|
|
||||||
},
|
|
||||||
"from": "FarmControl <farmcontrol@tombutcher.work>"
|
|
||||||
},
|
|
||||||
"otpExpiryMins": 0.5
|
"otpExpiryMins": 0.5
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
@ -105,16 +95,6 @@
|
|||||||
"filesBucket": "farmcontrol-test"
|
"filesBucket": "farmcontrol-test"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smtp": {
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 587,
|
|
||||||
"secure": false,
|
|
||||||
"auth": {
|
|
||||||
"user": "",
|
|
||||||
"pass": ""
|
|
||||||
},
|
|
||||||
"from": "FarmControl <farmcontrol@tombutcher.work>"
|
|
||||||
},
|
|
||||||
"otpExpiryMins": 0.5
|
"otpExpiryMins": 0.5
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
@ -163,16 +143,6 @@
|
|||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"filesBucket": "farmcontrol"
|
"filesBucket": "farmcontrol"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"smtp": {
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 587,
|
|
||||||
"secure": false,
|
|
||||||
"auth": {
|
|
||||||
"user": "",
|
|
||||||
"pass": ""
|
|
||||||
},
|
|
||||||
"from": "FarmControl <noreply@farmcontrol.app>"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,6 @@
|
|||||||
"nodemailer": "*",
|
"nodemailer": "*",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"puppeteer": "^24.37.5",
|
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"sequelize": "^6.37.7"
|
"sequelize": "^6.37.7"
|
||||||
},
|
},
|
||||||
|
|||||||
388
pnpm-lock.yaml
generated
388
pnpm-lock.yaml
generated
@ -86,9 +86,6 @@ importers:
|
|||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.3
|
specifier: ^8.16.3
|
||||||
version: 8.18.0
|
version: 8.18.0
|
||||||
puppeteer:
|
|
||||||
specifier: ^24.37.5
|
|
||||||
version: 24.37.5
|
|
||||||
redis:
|
redis:
|
||||||
specifier: ^5.10.0
|
specifier: ^5.10.0
|
||||||
version: 5.10.0
|
version: 5.10.0
|
||||||
@ -1187,11 +1184,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
'@puppeteer/browsers@2.13.0':
|
|
||||||
resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@redis/bloom@5.10.0':
|
'@redis/bloom@5.10.0':
|
||||||
resolution: {integrity: sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==}
|
resolution: {integrity: sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@ -1741,17 +1733,6 @@ packages:
|
|||||||
axios@1.13.4:
|
axios@1.13.4:
|
||||||
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
||||||
|
|
||||||
axios@1.13.6:
|
|
||||||
resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
|
|
||||||
|
|
||||||
b4a@1.8.0:
|
|
||||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
|
||||||
peerDependencies:
|
|
||||||
react-native-b4a: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react-native-b4a:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
babel-jest@30.2.0:
|
babel-jest@30.2.0:
|
||||||
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
|
resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@ -1800,44 +1781,6 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
bare-events@2.8.2:
|
|
||||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
|
||||||
peerDependencies:
|
|
||||||
bare-abort-controller: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bare-abort-controller:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
bare-fs@4.5.5:
|
|
||||||
resolution: {integrity: sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==}
|
|
||||||
engines: {bare: '>=1.16.0'}
|
|
||||||
peerDependencies:
|
|
||||||
bare-buffer: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bare-buffer:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
bare-os@3.7.0:
|
|
||||||
resolution: {integrity: sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==}
|
|
||||||
engines: {bare: '>=1.14.0'}
|
|
||||||
|
|
||||||
bare-path@3.0.0:
|
|
||||||
resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
|
|
||||||
|
|
||||||
bare-stream@2.8.0:
|
|
||||||
resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==}
|
|
||||||
peerDependencies:
|
|
||||||
bare-buffer: '*'
|
|
||||||
bare-events: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bare-buffer:
|
|
||||||
optional: true
|
|
||||||
bare-events:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
bare-url@2.3.2:
|
|
||||||
resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.9.19:
|
baseline-browser-mapping@2.9.19:
|
||||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -1845,7 +1788,6 @@ packages:
|
|||||||
basic-ftp@5.1.0:
|
basic-ftp@5.1.0:
|
||||||
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
|
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
deprecated: Security vulnerability fixed in 5.2.0, please upgrade
|
|
||||||
|
|
||||||
bcrypt@6.0.0:
|
bcrypt@6.0.0:
|
||||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
@ -1956,16 +1898,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
chromedriver@146.0.0:
|
chromedriver@145.0.0:
|
||||||
resolution: {integrity: sha512-fDAbuEy+Dn9F/h8fphiQIUEyUDOTGlfjZHfI9dJZz75+ui/LIHqWzStQt87vpwA9oV3ut4C2W3flfvbn3KELFQ==}
|
resolution: {integrity: sha512-rnqHS3u+OEdhaS3PmV7V8KYHBLiIOrIKMkRZSEaQcQXnpqHQTPBrS/1x7r0MJvuywtv2qFQYNbd5yXUmuxFvmg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
chromium-bidi@14.0.0:
|
|
||||||
resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==}
|
|
||||||
peerDependencies:
|
|
||||||
devtools-protocol: '*'
|
|
||||||
|
|
||||||
ci-info@4.4.0:
|
ci-info@4.4.0:
|
||||||
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
|
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2077,15 +2014,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
cosmiconfig@9.0.0:
|
|
||||||
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=4.9.5'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2183,9 +2111,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
devtools-protocol@0.0.1566079:
|
|
||||||
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
|
|
||||||
|
|
||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||||
|
|
||||||
@ -2245,10 +2170,6 @@ packages:
|
|||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
env-paths@2.2.1:
|
|
||||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||||
|
|
||||||
@ -2483,9 +2404,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
events-universal@1.0.1:
|
|
||||||
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
|
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2520,9 +2438,6 @@ packages:
|
|||||||
fast-diff@1.3.0:
|
fast-diff@1.3.0:
|
||||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||||
|
|
||||||
fast-fifo@1.3.2:
|
|
||||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
@ -3408,9 +3323,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
mitt@3.0.1:
|
|
||||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
|
||||||
|
|
||||||
mkdirp@0.5.6:
|
mkdirp@0.5.6:
|
||||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -3821,10 +3733,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==}
|
resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==}
|
||||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
|
||||||
progress@2.0.3:
|
|
||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
|
||||||
engines: {node: '>=0.4.0'}
|
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
@ -3842,9 +3750,6 @@ packages:
|
|||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
proxy-from-env@2.0.0:
|
|
||||||
resolution: {integrity: sha512-h2lD3OfRraP3R51rNFKIE8nX+qoLr1mE74X91YhVxtDbt+OD6ntoNZv56+JgI4RCdtwQ5eexsOk1KdOQDfvPCQ==}
|
|
||||||
|
|
||||||
pstree.remy@1.1.8:
|
pstree.remy@1.1.8:
|
||||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
@ -3855,15 +3760,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
puppeteer-core@24.37.5:
|
|
||||||
resolution: {integrity: sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
puppeteer@24.37.5:
|
|
||||||
resolution: {integrity: sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
pure-rand@7.0.1:
|
pure-rand@7.0.1:
|
||||||
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
||||||
|
|
||||||
@ -4018,11 +3914,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
semver@7.7.4:
|
|
||||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@ -4203,9 +4094,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
streamx@2.23.0:
|
|
||||||
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
|
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
|
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4295,25 +4183,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
|
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
tar-fs@3.1.1:
|
|
||||||
resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
|
|
||||||
|
|
||||||
tar-stream@3.1.8:
|
|
||||||
resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==}
|
|
||||||
|
|
||||||
tcp-port-used@1.0.2:
|
tcp-port-used@1.0.2:
|
||||||
resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==}
|
resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==}
|
||||||
|
|
||||||
teex@1.0.1:
|
|
||||||
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
|
|
||||||
|
|
||||||
test-exclude@6.0.0:
|
test-exclude@6.0.0:
|
||||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
text-decoder@1.2.7:
|
|
||||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
|
||||||
|
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
@ -4396,9 +4272,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
typed-query-selector@2.12.1:
|
|
||||||
resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==}
|
|
||||||
|
|
||||||
typedarray@0.0.6:
|
typedarray@0.0.6:
|
||||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
|
|
||||||
@ -4490,9 +4363,6 @@ packages:
|
|||||||
walker@1.0.8:
|
walker@1.0.8:
|
||||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||||
|
|
||||||
webdriver-bidi-protocol@0.4.1:
|
|
||||||
resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==}
|
|
||||||
|
|
||||||
webidl-conversions@7.0.0:
|
webidl-conversions@7.0.0:
|
||||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -4544,18 +4414,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
ws@8.19.0:
|
|
||||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
bufferutil: ^4.0.1
|
|
||||||
utf-8-validate: '>=5.0.2'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
bufferutil:
|
|
||||||
optional: true
|
|
||||||
utf-8-validate:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
xdg-basedir@4.0.0:
|
xdg-basedir@4.0.0:
|
||||||
resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==}
|
resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -4597,9 +4455,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
zod@3.25.76:
|
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@aws-crypto/crc32@5.2.0':
|
'@aws-crypto/crc32@5.2.0':
|
||||||
@ -6266,21 +6121,6 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.9': {}
|
'@pkgr/core@0.2.9': {}
|
||||||
|
|
||||||
'@puppeteer/browsers@2.13.0':
|
|
||||||
dependencies:
|
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
|
||||||
extract-zip: 2.0.1
|
|
||||||
progress: 2.0.3
|
|
||||||
proxy-agent: 6.5.0
|
|
||||||
semver: 7.7.4
|
|
||||||
tar-fs: 3.1.1
|
|
||||||
yargs: 17.7.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- bare-buffer
|
|
||||||
- react-native-b4a
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@redis/bloom@5.10.0(@redis/client@5.10.0)':
|
'@redis/bloom@5.10.0(@redis/client@5.10.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@redis/client': 5.10.0
|
'@redis/client': 5.10.0
|
||||||
@ -6654,7 +6494,8 @@ snapshots:
|
|||||||
'@testim/chrome-version@1.1.4':
|
'@testim/chrome-version@1.1.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
'@tootallnate/quickjs-emscripten@0.23.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6803,7 +6644,8 @@ snapshots:
|
|||||||
|
|
||||||
acorn@8.15.0: {}
|
acorn@8.15.0: {}
|
||||||
|
|
||||||
agent-base@7.1.4: {}
|
agent-base@7.1.4:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6931,6 +6773,7 @@ snapshots:
|
|||||||
ast-types@0.13.4:
|
ast-types@0.13.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
@ -6950,17 +6793,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
axios@1.13.6:
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.15.11
|
|
||||||
form-data: 4.0.5
|
|
||||||
proxy-from-env: 1.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
b4a@1.8.0: {}
|
|
||||||
|
|
||||||
babel-jest@30.2.0(@babel/core@7.29.0):
|
babel-jest@30.2.0(@babel/core@7.29.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
@ -7045,42 +6877,10 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
bare-events@2.8.2: {}
|
|
||||||
|
|
||||||
bare-fs@4.5.5:
|
|
||||||
dependencies:
|
|
||||||
bare-events: 2.8.2
|
|
||||||
bare-path: 3.0.0
|
|
||||||
bare-stream: 2.8.0(bare-events@2.8.2)
|
|
||||||
bare-url: 2.3.2
|
|
||||||
fast-fifo: 1.3.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
bare-os@3.7.0: {}
|
|
||||||
|
|
||||||
bare-path@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
bare-os: 3.7.0
|
|
||||||
|
|
||||||
bare-stream@2.8.0(bare-events@2.8.2):
|
|
||||||
dependencies:
|
|
||||||
streamx: 2.23.0
|
|
||||||
teex: 1.0.1
|
|
||||||
optionalDependencies:
|
|
||||||
bare-events: 2.8.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
bare-url@2.3.2:
|
|
||||||
dependencies:
|
|
||||||
bare-path: 3.0.0
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.9.19: {}
|
baseline-browser-mapping@2.9.19: {}
|
||||||
|
|
||||||
basic-ftp@5.1.0: {}
|
basic-ftp@5.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
bcrypt@6.0.0:
|
bcrypt@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7138,7 +6938,8 @@ snapshots:
|
|||||||
|
|
||||||
bson@6.10.4: {}
|
bson@6.10.4: {}
|
||||||
|
|
||||||
buffer-crc32@0.2.13: {}
|
buffer-crc32@0.2.13:
|
||||||
|
optional: true
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
@ -7200,26 +7001,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
chromedriver@146.0.0:
|
chromedriver@145.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@testim/chrome-version': 1.1.4
|
'@testim/chrome-version': 1.1.4
|
||||||
axios: 1.13.6
|
axios: 1.13.4
|
||||||
compare-versions: 6.1.1
|
compare-versions: 6.1.1
|
||||||
extract-zip: 2.0.1
|
extract-zip: 2.0.1
|
||||||
proxy-agent: 6.5.0
|
proxy-agent: 6.5.0
|
||||||
proxy-from-env: 2.0.0
|
proxy-from-env: 1.1.0
|
||||||
tcp-port-used: 1.0.2
|
tcp-port-used: 1.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
chromium-bidi@14.0.0(devtools-protocol@0.0.1566079):
|
|
||||||
dependencies:
|
|
||||||
devtools-protocol: 0.0.1566079
|
|
||||||
mitt: 3.0.1
|
|
||||||
zod: 3.25.76
|
|
||||||
|
|
||||||
ci-info@4.4.0: {}
|
ci-info@4.4.0: {}
|
||||||
|
|
||||||
cjs-module-lexer@2.2.0: {}
|
cjs-module-lexer@2.2.0: {}
|
||||||
@ -7319,20 +7114,14 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
cosmiconfig@9.0.0:
|
|
||||||
dependencies:
|
|
||||||
env-paths: 2.2.1
|
|
||||||
import-fresh: 3.3.1
|
|
||||||
js-yaml: 4.1.1
|
|
||||||
parse-json: 5.2.0
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
data-uri-to-buffer@6.0.2: {}
|
data-uri-to-buffer@6.0.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
data-view-buffer@1.0.2:
|
data-view-buffer@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7396,6 +7185,7 @@ snapshots:
|
|||||||
ast-types: 0.13.4
|
ast-types: 0.13.4
|
||||||
escodegen: 2.1.0
|
escodegen: 2.1.0
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
@ -7403,8 +7193,6 @@ snapshots:
|
|||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
|
|
||||||
devtools-protocol@0.0.1566079: {}
|
|
||||||
|
|
||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
asap: 2.0.6
|
asap: 2.0.6
|
||||||
@ -7466,8 +7254,7 @@ snapshots:
|
|||||||
end-of-stream@1.4.5:
|
end-of-stream@1.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
optional: true
|
||||||
env-paths@2.2.1: {}
|
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7591,6 +7378,7 @@ snapshots:
|
|||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@9.39.2):
|
eslint-config-prettier@10.1.8(eslint@9.39.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7601,7 +7389,7 @@ snapshots:
|
|||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
|
|
||||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
|
eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
||||||
@ -7842,12 +7630,6 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
events-universal@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
bare-events: 2.8.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@ -7928,13 +7710,12 @@ snapshots:
|
|||||||
'@types/yauzl': 2.10.3
|
'@types/yauzl': 2.10.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-diff@1.3.0: {}
|
fast-diff@1.3.0: {}
|
||||||
|
|
||||||
fast-fifo@1.3.2: {}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
@ -7956,6 +7737,7 @@ snapshots:
|
|||||||
fd-slicer@1.1.0:
|
fd-slicer@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pend: 1.2.0
|
pend: 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
file-entry-cache@6.0.1:
|
file-entry-cache@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8106,6 +7888,7 @@ snapshots:
|
|||||||
get-stream@5.2.0:
|
get-stream@5.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
get-stream@6.0.1: {}
|
get-stream@6.0.1: {}
|
||||||
|
|
||||||
@ -8122,6 +7905,7 @@ snapshots:
|
|||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8221,6 +8005,7 @@ snapshots:
|
|||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8228,6 +8013,7 @@ snapshots:
|
|||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
human-signals@2.1.0: {}
|
human-signals@2.1.0: {}
|
||||||
|
|
||||||
@ -8270,7 +8056,8 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
ip-address@10.1.0: {}
|
ip-address@10.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ip-regex@4.3.0:
|
ip-regex@4.3.0:
|
||||||
optional: true
|
optional: true
|
||||||
@ -8877,7 +8664,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jwk-to-pem: 2.0.7
|
jwk-to-pem: 2.0.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
chromedriver: 146.0.0
|
chromedriver: 145.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -8958,7 +8745,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
lru-cache@7.18.3: {}
|
lru-cache@7.18.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
make-dir@2.1.0:
|
make-dir@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9028,8 +8816,6 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
mitt@3.0.1: {}
|
|
||||||
|
|
||||||
mkdirp@0.5.6:
|
mkdirp@0.5.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
@ -9115,7 +8901,8 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
netmask@2.0.2: {}
|
netmask@2.0.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-addon-api@8.5.0: {}
|
node-addon-api@8.5.0: {}
|
||||||
|
|
||||||
@ -9276,11 +9063,13 @@ snapshots:
|
|||||||
socks-proxy-agent: 8.0.5
|
socks-proxy-agent: 8.0.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
pac-resolver@7.0.1:
|
pac-resolver@7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
degenerator: 5.0.1
|
degenerator: 5.0.1
|
||||||
netmask: 2.0.2
|
netmask: 2.0.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
@ -9321,7 +9110,8 @@ snapshots:
|
|||||||
|
|
||||||
path-to-regexp@8.3.0: {}
|
path-to-regexp@8.3.0: {}
|
||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pg-cloudflare@1.3.0:
|
pg-cloudflare@1.3.0:
|
||||||
optional: true
|
optional: true
|
||||||
@ -9407,8 +9197,6 @@ snapshots:
|
|||||||
ansi-styles: 5.2.0
|
ansi-styles: 5.2.0
|
||||||
react-is: 18.3.1
|
react-is: 18.3.1
|
||||||
|
|
||||||
progress@2.0.3: {}
|
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@ -9434,55 +9222,20 @@ snapshots:
|
|||||||
socks-proxy-agent: 8.0.5
|
socks-proxy-agent: 8.0.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
proxy-from-env@2.0.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pstree.remy@1.1.8: {}
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
puppeteer-core@24.37.5:
|
|
||||||
dependencies:
|
|
||||||
'@puppeteer/browsers': 2.13.0
|
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079)
|
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
|
||||||
devtools-protocol: 0.0.1566079
|
|
||||||
typed-query-selector: 2.12.1
|
|
||||||
webdriver-bidi-protocol: 0.4.1
|
|
||||||
ws: 8.19.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- bare-buffer
|
|
||||||
- bufferutil
|
|
||||||
- react-native-b4a
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
puppeteer@24.37.5:
|
|
||||||
dependencies:
|
|
||||||
'@puppeteer/browsers': 2.13.0
|
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1566079)
|
|
||||||
cosmiconfig: 9.0.0
|
|
||||||
devtools-protocol: 0.0.1566079
|
|
||||||
puppeteer-core: 24.37.5
|
|
||||||
typed-query-selector: 2.12.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- bare-buffer
|
|
||||||
- bufferutil
|
|
||||||
- react-native-b4a
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
pure-rand@7.0.1: {}
|
pure-rand@7.0.1: {}
|
||||||
|
|
||||||
qs@6.14.1:
|
qs@6.14.1:
|
||||||
@ -9648,8 +9401,6 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.7.3: {}
|
semver@7.7.3: {}
|
||||||
|
|
||||||
semver@7.7.4: {}
|
|
||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
@ -9788,7 +9539,8 @@ snapshots:
|
|||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9797,11 +9549,13 @@ snapshots:
|
|||||||
socks: 2.8.7
|
socks: 2.8.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
socks@2.8.7:
|
socks@2.8.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
ip-address: 10.1.0
|
ip-address: 10.1.0
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map-support@0.5.13:
|
source-map-support@0.5.13:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9837,7 +9591,7 @@ snapshots:
|
|||||||
standard@17.1.2:
|
standard@17.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
|
eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@9.39.2))(eslint-plugin-promise@6.6.0(eslint@9.39.2))(eslint@8.57.1)
|
||||||
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1)
|
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(eslint@8.57.1)
|
||||||
eslint-plugin-n: 15.7.0(eslint@8.57.1)
|
eslint-plugin-n: 15.7.0(eslint@8.57.1)
|
||||||
@ -9868,15 +9622,6 @@ snapshots:
|
|||||||
|
|
||||||
streamsearch@1.1.0: {}
|
streamsearch@1.1.0: {}
|
||||||
|
|
||||||
streamx@2.23.0:
|
|
||||||
dependencies:
|
|
||||||
events-universal: 1.0.1
|
|
||||||
fast-fifo: 1.3.2
|
|
||||||
text-decoder: 1.2.7
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
char-regex: 1.0.2
|
char-regex: 1.0.2
|
||||||
@ -10000,29 +9745,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.2.9
|
'@pkgr/core': 0.2.9
|
||||||
|
|
||||||
tar-fs@3.1.1:
|
|
||||||
dependencies:
|
|
||||||
pump: 3.0.3
|
|
||||||
tar-stream: 3.1.8
|
|
||||||
optionalDependencies:
|
|
||||||
bare-fs: 4.5.5
|
|
||||||
bare-path: 3.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- bare-buffer
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
tar-stream@3.1.8:
|
|
||||||
dependencies:
|
|
||||||
b4a: 1.8.0
|
|
||||||
bare-fs: 4.5.5
|
|
||||||
fast-fifo: 1.3.2
|
|
||||||
streamx: 2.23.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- bare-buffer
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
tcp-port-used@1.0.2:
|
tcp-port-used@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.1
|
debug: 4.3.1
|
||||||
@ -10031,25 +9753,12 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
teex@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
streamx: 2.23.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bare-abort-controller
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
test-exclude@6.0.0:
|
test-exclude@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@istanbuljs/schema': 0.1.3
|
'@istanbuljs/schema': 0.1.3
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
|
|
||||||
text-decoder@1.2.7:
|
|
||||||
dependencies:
|
|
||||||
b4a: 1.8.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- react-native-b4a
|
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
tmpl@1.0.5: {}
|
tmpl@1.0.5: {}
|
||||||
@ -10137,8 +9846,6 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
typed-query-selector@2.12.1: {}
|
|
||||||
|
|
||||||
typedarray@0.0.6: {}
|
typedarray@0.0.6: {}
|
||||||
|
|
||||||
uid-safe@2.1.5:
|
uid-safe@2.1.5:
|
||||||
@ -10235,8 +9942,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
makeerror: 1.0.12
|
makeerror: 1.0.12
|
||||||
|
|
||||||
webdriver-bidi-protocol@0.4.1: {}
|
|
||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
||||||
whatwg-url@14.2.0:
|
whatwg-url@14.2.0:
|
||||||
@ -10314,8 +10019,6 @@ snapshots:
|
|||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
ws@8.19.0: {}
|
|
||||||
|
|
||||||
xdg-basedir@4.0.0: {}
|
xdg-basedir@4.0.0: {}
|
||||||
|
|
||||||
xml@1.0.1: {}
|
xml@1.0.1: {}
|
||||||
@ -10354,7 +10057,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
buffer-crc32: 0.2.13
|
buffer-crc32: 0.2.13
|
||||||
fd-slicer: 1.1.0
|
fd-slicer: 1.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
|
||||||
|
|||||||
@ -53,22 +53,6 @@ function loadConfig() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure smtp config exists and override with env vars if available
|
|
||||||
if (!envConfig.smtp) {
|
|
||||||
envConfig.smtp = {};
|
|
||||||
}
|
|
||||||
if (process.env.SMTP_HOST) envConfig.smtp.host = process.env.SMTP_HOST;
|
|
||||||
if (process.env.SMTP_PORT) envConfig.smtp.port = parseInt(process.env.SMTP_PORT, 10);
|
|
||||||
if (process.env.SMTP_SECURE) envConfig.smtp.secure = process.env.SMTP_SECURE === 'true';
|
|
||||||
if (process.env.SMTP_USER || process.env.SMTP_PASS) {
|
|
||||||
envConfig.smtp.auth = {
|
|
||||||
...(envConfig.smtp.auth || {}),
|
|
||||||
...(process.env.SMTP_USER && { user: process.env.SMTP_USER }),
|
|
||||||
...(process.env.SMTP_PASS && { pass: process.env.SMTP_PASS }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (process.env.SMTP_FROM) envConfig.smtp.from = process.env.SMTP_FROM;
|
|
||||||
|
|
||||||
return envConfig;
|
return envConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading config:', err);
|
console.error('Error loading config:', err);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
|
import { expressSession, keycloak } from './keycloak.js';
|
||||||
import { dbConnect } from './database/mongo.js';
|
import { dbConnect } from './database/mongo.js';
|
||||||
import {
|
import {
|
||||||
authRoutes,
|
authRoutes,
|
||||||
@ -104,6 +105,8 @@ async function initializeApp() {
|
|||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.use(bodyParser.json({ type: 'application/json', strict: false, limit: '50mb' }));
|
app.use(bodyParser.json({ type: 'application/json', strict: false, limit: '50mb' }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(expressSession);
|
||||||
|
app.use(keycloak.middleware());
|
||||||
app.use(populateUserMiddleware);
|
app.use(populateUserMiddleware);
|
||||||
|
|
||||||
app.get('/', function (req, res) {
|
app.get('/', function (req, res) {
|
||||||
|
|||||||
129
src/keycloak.js
129
src/keycloak.js
@ -1,20 +1,21 @@
|
|||||||
/**
|
import Keycloak from 'keycloak-connect';
|
||||||
* Authentication middleware - uses Redis session store.
|
import session from 'express-session';
|
||||||
* Keycloak is used only for login/refresh; session validation is done via Redis.
|
|
||||||
*/
|
|
||||||
import config, { getEnvironment } from './config.js';
|
import config, { getEnvironment } from './config.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
import { userModel } from './database/schemas/management/user.schema.js';
|
import { userModel } from './database/schemas/management/user.schema.js';
|
||||||
import { getObject } from './database/database.js';
|
import { getObject } from './database/database.js';
|
||||||
import { hostModel } from './database/schemas/management/host.schema.js';
|
import { hostModel } from './database/schemas/management/host.schema.js';
|
||||||
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Keycloak');
|
const logger = log4js.getLogger('Keycloak');
|
||||||
logger.level = config.server.logLevel || 'info';
|
logger.level = config.server.logLevel || 'info';
|
||||||
|
|
||||||
const userCache = new NodeCache({ stdTTL: 300 });
|
// Initialize NodeCache with 5-minute TTL
|
||||||
|
const userCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes
|
||||||
|
|
||||||
|
// Cache event listeners for monitoring
|
||||||
userCache.on('expired', (key, value) => {
|
userCache.on('expired', (key, value) => {
|
||||||
logger.debug(`Cache entry expired: ${key}`);
|
logger.debug(`Cache entry expired: ${key}`);
|
||||||
});
|
});
|
||||||
@ -23,18 +24,22 @@ userCache.on('flush', () => {
|
|||||||
logger.info('Cache flushed');
|
logger.info('Cache flushed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User lookup function with caching
|
||||||
const lookupUser = async (preferredUsername) => {
|
const lookupUser = async (preferredUsername) => {
|
||||||
try {
|
try {
|
||||||
|
// Check cache first
|
||||||
const cachedUser = userCache.get(preferredUsername);
|
const cachedUser = userCache.get(preferredUsername);
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
logger.debug(`User found in cache: ${preferredUsername}`);
|
logger.debug(`User found in cache: ${preferredUsername}`);
|
||||||
return cachedUser;
|
return cachedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not in cache, query database
|
||||||
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
||||||
const user = await userModel.findOne({ username: preferredUsername });
|
const user = await userModel.findOne({ username: preferredUsername });
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Store in cache
|
||||||
userCache.set(preferredUsername, user);
|
userCache.set(preferredUsername, user);
|
||||||
logger.debug(`User stored in cache: ${preferredUsername}`);
|
logger.debug(`User stored in cache: ${preferredUsername}`);
|
||||||
return user;
|
return user;
|
||||||
@ -48,32 +53,71 @@ const lookupUser = async (preferredUsername) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Initialize Keycloak
|
||||||
* Middleware to check if the user is authenticated.
|
const keycloakConfig = {
|
||||||
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer), 3) x-host-id + x-auth-code (host auth)
|
realm: config.auth.keycloak.realm,
|
||||||
*/
|
'auth-server-url': config.auth.keycloak.url,
|
||||||
|
'ssl-required': getEnvironment() === 'production' ? 'external' : 'none',
|
||||||
|
resource: config.auth.keycloak.clientId,
|
||||||
|
'confidential-port': 0,
|
||||||
|
'bearer-only': true,
|
||||||
|
'public-client': false,
|
||||||
|
'use-resource-role-mappings': true,
|
||||||
|
'verify-token-audience': true,
|
||||||
|
credentials: {
|
||||||
|
secret: config.auth.keycloak.clientSecret,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoryStore = new session.MemoryStore();
|
||||||
|
|
||||||
|
var expressSession = session({
|
||||||
|
secret: config.auth.sessionSecret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true, // Set this to true to ensure session is initialized
|
||||||
|
store: memoryStore,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1800000, // 30 minutes
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var keycloak = new Keycloak({ store: memoryStore }, keycloakConfig);
|
||||||
|
|
||||||
|
// Custom middleware to check if the user is authenticated
|
||||||
const isAuthenticated = async (req, res, next) => {
|
const isAuthenticated = async (req, res, next) => {
|
||||||
|
let token = null;
|
||||||
|
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
const token = authHeader.substring(7);
|
token = authHeader.substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await getSession(token);
|
// Verify token with Keycloak introspection endpoint
|
||||||
if (session && session.expiresAt > Date.now()) {
|
const response = await axios.post(
|
||||||
req.user = session.user;
|
`${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
||||||
req.session = session;
|
new URLSearchParams({
|
||||||
return next();
|
token: token,
|
||||||
|
client_id: config.auth.keycloak.clientId,
|
||||||
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const introspection = response.data;
|
||||||
|
if (!introspection.active) {
|
||||||
|
logger.info('Token is not active');
|
||||||
|
logger.debug('Token:', token);
|
||||||
|
return res.status(401).json({ error: 'Session Inactive', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
|
||||||
const user = await lookupUserByToken(token);
|
|
||||||
if (user) {
|
|
||||||
req.user = user;
|
|
||||||
req.session = { user };
|
|
||||||
return next();
|
return next();
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Session lookup error:', error.message);
|
logger.error('Token verification error:', error.message);
|
||||||
|
return res.status(401).json({ error: 'Verification Error', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,14 +125,46 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
const authCode = req.headers['x-auth-code'];
|
const authCode = req.headers['x-auth-code'];
|
||||||
if (hostId && authCode) {
|
if (hostId && authCode) {
|
||||||
const host = await getObject({ model: hostModel, id: hostId });
|
const host = await getObject({ model: hostModel, id: hostId });
|
||||||
if (host && host.authCode === authCode) {
|
if (host && host.authCode == authCode) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to session-based authentication
|
||||||
|
if (req.session && req.session['keycloak-token']) {
|
||||||
|
const sessionToken = req.session['keycloak-token'];
|
||||||
|
if (sessionToken.expires_at > new Date().getTime()) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
|
||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to extract roles from token
|
||||||
|
function extractRoles(token) {
|
||||||
|
const roles = [];
|
||||||
|
|
||||||
|
// Extract realm roles
|
||||||
|
if (token.realm_access && token.realm_access.roles) {
|
||||||
|
roles.push(...token.realm_access.roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract client roles
|
||||||
|
if (token.resource_access) {
|
||||||
|
for (const client in token.resource_access) {
|
||||||
|
if (token.resource_access[client].roles) {
|
||||||
|
roles.push(...token.resource_access[client].roles.map((role) => `${client}:${role}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache management utility functions
|
||||||
const clearUserCache = () => {
|
const clearUserCache = () => {
|
||||||
userCache.flushAll();
|
userCache.flushAll();
|
||||||
logger.info('User cache cleared');
|
logger.info('User cache cleared');
|
||||||
@ -104,10 +180,11 @@ const removeUserFromCache = (username) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
keycloak,
|
||||||
|
expressSession,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
lookupUser,
|
lookupUser,
|
||||||
clearUserCache,
|
clearUserCache,
|
||||||
getUserCacheStats,
|
getUserCacheStats,
|
||||||
removeUserFromCache,
|
removeUserFromCache,
|
||||||
getEnvironment,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
/**
|
|
||||||
* Worker thread for sending email notifications asynchronously.
|
|
||||||
* Receives payloads from the main thread and performs Puppeteer render + nodemailer send.
|
|
||||||
*/
|
|
||||||
import { parentPort } from 'worker_threads';
|
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import config from './config.js';
|
|
||||||
|
|
||||||
const baseUrl = (urlClient) => (urlClient || 'http://localhost:3000').replace(/\/$/, '');
|
|
||||||
|
|
||||||
async function fetchAndInlineStyles(html, urlClient) {
|
|
||||||
const base = baseUrl(urlClient);
|
|
||||||
const linkMatches = [...html.matchAll(/<link[^>]+>/g)];
|
|
||||||
const stylesheetLinks = linkMatches
|
|
||||||
.map((m) => {
|
|
||||||
const tag = m[0];
|
|
||||||
if (!/rel=["']stylesheet["']/i.test(tag)) return null;
|
|
||||||
const hrefMatch = tag.match(/href=["']([^"']+)["']/);
|
|
||||||
return hrefMatch ? { tag, href: hrefMatch[1] } : null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
let inlined = html;
|
|
||||||
for (const { tag, href } of stylesheetLinks) {
|
|
||||||
const url = href.startsWith('http') ? href : `${base}${href.startsWith('/') ? '' : '/'}${href}`;
|
|
||||||
try {
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (res.ok) {
|
|
||||||
const css = await res.text();
|
|
||||||
inlined = inlined.replace(tag, `<style>${css}</style>`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.trace('Could not fetch stylesheet:', url, e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inlined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('MailWorker');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
async function sendEmail(payload) {
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
metadata,
|
|
||||||
smtpConfig,
|
|
||||||
urlClient,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
authCode,
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
if (!email || !smtpConfig?.host) {
|
|
||||||
logger.warn('Missing email or SMTP config, skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
title: title || '',
|
|
||||||
message: message || '',
|
|
||||||
type: type || 'info',
|
|
||||||
email: email || '',
|
|
||||||
createdAt: createdAt || new Date(),
|
|
||||||
updatedAt: updatedAt || new Date(),
|
|
||||||
authCode: authCode || '',
|
|
||||||
metadata: JSON.stringify(metadata || {}),
|
|
||||||
});
|
|
||||||
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
|
||||||
|
|
||||||
logger.debug('Rendering template...');
|
|
||||||
logger.trace('Template URL:', templateUrl);
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
let browser;
|
|
||||||
try {
|
|
||||||
browser = await puppeteer.launch({
|
|
||||||
headless: 'new',
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--disable-features=SameSiteByDefaultCookies',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const page = await browser.newPage();
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
const text = msg.text();
|
|
||||||
const type = msg.type();
|
|
||||||
logger.trace(`Puppeteer [${type}]: ${text}`);
|
|
||||||
});
|
|
||||||
await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
||||||
await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 });
|
|
||||||
// Wait for Ant Design CSS-in-JS to finish injecting styles
|
|
||||||
logger.debug('Waiting for 1.5 seconds for page to render...');
|
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
|
||||||
html = await page.evaluate(() => {
|
|
||||||
const root = document.getElementById('email-notification-root');
|
|
||||||
if (!root) return document.documentElement.outerHTML;
|
|
||||||
const origin = document.location.origin;
|
|
||||||
const styleTags = Array.from(document.querySelectorAll('style'))
|
|
||||||
.map((s) => s.outerHTML)
|
|
||||||
.join('\n');
|
|
||||||
const linkTags = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
||||||
.map((link) => {
|
|
||||||
const href = link.getAttribute('href');
|
|
||||||
const abs = href?.startsWith('http')
|
|
||||||
? href
|
|
||||||
: `${origin}${href?.startsWith('/') ? '' : '/'}${href || ''}`;
|
|
||||||
return `<link rel="stylesheet" href="${abs}">`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${styleTags}${linkTags}</head><body>${root.outerHTML}</body></html>`;
|
|
||||||
});
|
|
||||||
html = await fetchAndInlineStyles(html, urlClient);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('MailWorker: Puppeteer error', err.message);
|
|
||||||
html = `<div style="font-family:sans-serif;padding:20px"><h2>${title || 'Notification'}</h2><p>${message || ''}</p></div>`;
|
|
||||||
} finally {
|
|
||||||
if (browser) await browser.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: smtpConfig.host,
|
|
||||||
port: smtpConfig.port || 587,
|
|
||||||
secure: smtpConfig.secure || false,
|
|
||||||
auth: smtpConfig.auth?.user ? smtpConfig.auth : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const subject = title ? `${title} - FarmControl` : 'FarmControl Notification';
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: smtpConfig.from || 'FarmControl <noreply@tombutcher.work>',
|
|
||||||
to: email,
|
|
||||||
subject: subject,
|
|
||||||
html,
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug('Sending email...');
|
|
||||||
logger.trace('Mail options:', mailOptions);
|
|
||||||
const info = await transporter.sendMail(mailOptions);
|
|
||||||
logger.debug('Email sent successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
parentPort.on('message', (payload) => {
|
|
||||||
sendEmail(payload).catch((err) => {
|
|
||||||
logger.error('MailWorker: send failed', err.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import config from '../../config.js';
|
import config from '../../config.js';
|
||||||
|
import { keycloak } from '../../keycloak.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { userModel } from '../../database/schemas/management/user.schema.js';
|
import { userModel } from '../../database/schemas/management/user.schema.js';
|
||||||
@ -6,20 +7,14 @@ import { readFileSync } from 'fs';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { getAndConsumeEmailRenderTokenData } from './emailRenderAuth.js';
|
|
||||||
import {
|
|
||||||
createSession,
|
|
||||||
getSession,
|
|
||||||
updateSessionKeycloakTokens,
|
|
||||||
deleteSession,
|
|
||||||
} from './sessionStore.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Auth');
|
const logger = log4js.getLogger('Auth');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
// Initialize NodeCache with 5-minute TTL for token-based user lookup (email render tokens)
|
// Initialize NodeCache with 5-minute TTL for token-based user lookup
|
||||||
const tokenUserCache = new NodeCache({ stdTTL: 300 });
|
const tokenUserCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes
|
||||||
|
|
||||||
|
// Cache event listeners for monitoring
|
||||||
tokenUserCache.on('expired', (key, value) => {
|
tokenUserCache.on('expired', (key, value) => {
|
||||||
logger.debug(`Token user cache entry expired: ${key.substring(0, 20)}...`);
|
logger.debug(`Token user cache entry expired: ${key.substring(0, 20)}...`);
|
||||||
});
|
});
|
||||||
@ -30,24 +25,32 @@ tokenUserCache.on('flush', () => {
|
|||||||
|
|
||||||
const loginTokenRequests = new Map();
|
const loginTokenRequests = new Map();
|
||||||
|
|
||||||
// Lookup user by email-render JWT token (short-lived, for Puppeteer)
|
// Token-based user lookup function with caching
|
||||||
const lookupUserByEmailRenderToken = async (token) => {
|
const lookupUserByToken = async (token) => {
|
||||||
try {
|
try {
|
||||||
|
// Check cache first
|
||||||
const cachedUser = tokenUserCache.get(token);
|
const cachedUser = tokenUserCache.get(token);
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
logger.trace(`User found in token cache for token: ${token.substring(0, 20)}...`);
|
logger.trace(`User found in token cache for token: ${token.substring(0, 20)}...`);
|
||||||
return cachedUser;
|
return cachedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.verify(token, config.auth.sessionSecret);
|
// If not in cache, decode token and lookup user
|
||||||
|
logger.trace(`User not in token cache, decoding token: ${token.substring(0, 20)}...`);
|
||||||
|
const decodedToken = jwt.decode(token);
|
||||||
|
|
||||||
if (!decodedToken || !decodedToken.preferred_username) {
|
if (!decodedToken || !decodedToken.preferred_username) {
|
||||||
logger.trace('Invalid token or missing preferred_username');
|
logger.trace('Invalid token or missing preferred_username');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query database for user
|
||||||
const user = await userModel.findOne({ username: decodedToken.preferred_username });
|
const user = await userModel.findOne({ username: decodedToken.preferred_username });
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Store in cache using token as key
|
||||||
tokenUserCache.set(token, user);
|
tokenUserCache.set(token, user);
|
||||||
|
logger.trace(`User stored in token cache for token: ${token.substring(0, 20)}...`);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +62,7 @@ const lookupUserByEmailRenderToken = async (token) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cache management utility functions
|
||||||
const clearTokenUserCache = () => {
|
const clearTokenUserCache = () => {
|
||||||
tokenUserCache.flushAll();
|
tokenUserCache.flushAll();
|
||||||
logger.info('Token user cache cleared');
|
logger.info('Token user cache cleared');
|
||||||
@ -73,23 +77,29 @@ const removeUserFromTokenCache = (token) => {
|
|||||||
logger.debug(`User removed from token cache for token: ${token.substring(0, 20)}...`);
|
logger.debug(`User removed from token cache for token: ${token.substring(0, 20)}...`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login handler - redirect to Keycloak
|
// Login handler
|
||||||
export const loginRouteHandler = (req, res, redirectType = 'web') => {
|
export const loginRouteHandler = (req, res, redirectType = 'web') => {
|
||||||
|
// Get the redirect URL from form data or default to production overview
|
||||||
const redirectUrl = req.query.redirect_uri || '/production/overview';
|
const redirectUrl = req.query.redirect_uri || '/production/overview';
|
||||||
|
|
||||||
|
// Store the original URL to redirect after login
|
||||||
const authUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/auth`;
|
const authUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/auth`;
|
||||||
const callBackState = `/auth/${redirectType}/callback`;
|
const callBackState = `/auth/${redirectType}/callback`;
|
||||||
const callbackUrl = `${config.app.urlApi}${callBackState}`;
|
const callbackUrl = `${config.app.urlApi}${callBackState}`;
|
||||||
const state = encodeURIComponent(redirectUrl);
|
const state = encodeURIComponent(redirectUrl);
|
||||||
|
|
||||||
|
logger.warn(req.query.redirect_uri);
|
||||||
|
|
||||||
res.redirect(
|
res.redirect(
|
||||||
`${authUrl}?client_id=${config.auth.keycloak.clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=openid&state=${state}`
|
`${authUrl}?client_id=${config.auth.keycloak.clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=openid&state=${state}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch user from Keycloak and create/update in database
|
// Function to fetch user from Keycloak and store in database and session
|
||||||
const fetchAndStoreUser = async (keycloakTokenData) => {
|
const fetchAndStoreUser = async (req, token) => {
|
||||||
const userInfoUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/userinfo`;
|
const userInfoUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/userinfo`;
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
userInfoUrl,
|
userInfoUrl,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
@ -98,16 +108,13 @@ const fetchAndStoreUser = async (keycloakTokenData) => {
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${keycloakTokenData.access_token}`,
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const decoded = jwt.decode(keycloakTokenData.access_token);
|
|
||||||
const roles = decoded?.realm_access?.roles || [];
|
|
||||||
|
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
roles,
|
roles: token.realm_access?.roles || [],
|
||||||
username: response.data.preferred_username,
|
username: response.data.preferred_username,
|
||||||
email: response.data.email,
|
email: response.data.email,
|
||||||
name: response.data.name,
|
name: response.data.name,
|
||||||
@ -115,11 +122,21 @@ const fetchAndStoreUser = async (keycloakTokenData) => {
|
|||||||
lastName: response.data.family_name,
|
lastName: response.data.family_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create or update user in database
|
||||||
const user = await createOrUpdateUser(userInfo);
|
const user = await createOrUpdateUser(userInfo);
|
||||||
return { ...userInfo, _id: user._id };
|
const fullUserInfo = { ...userInfo, _id: user._id };
|
||||||
|
|
||||||
|
// Store user info in session
|
||||||
|
req.session.user = fullUserInfo;
|
||||||
|
|
||||||
|
return fullUserInfo;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching and storing user:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exchange auth code for tokens, create Redis session, return our session token to client
|
// Function to exchange authorization code for tokens, fetch user, and set session
|
||||||
export const loginTokenRouteHandler = async (req, res, redirectType = 'web') => {
|
export const loginTokenRouteHandler = async (req, res, redirectType = 'web') => {
|
||||||
const code = req.query.code;
|
const code = req.query.code;
|
||||||
if (!code) {
|
if (!code) {
|
||||||
@ -127,18 +144,13 @@ export const loginTokenRouteHandler = async (req, res, redirectType = 'web') =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check for temporary email render auth code (30s TTL for Puppeteer)
|
// If a request for this code is already in progress, wait for it
|
||||||
const emailRenderData = getAndConsumeEmailRenderTokenData(code);
|
|
||||||
if (emailRenderData) {
|
|
||||||
logger.debug('Exchanged email render auth code for token');
|
|
||||||
return res.status(200).json(emailRenderData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginTokenRequests.has(code)) {
|
if (loginTokenRequests.has(code)) {
|
||||||
const tokenData = await loginTokenRequests.get(code);
|
const tokenData = await loginTokenRequests.get(code);
|
||||||
return res.status(200).json(tokenData);
|
return res.status(200).json(tokenData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, start the request and store the promise
|
||||||
const tokenPromise = (async () => {
|
const tokenPromise = (async () => {
|
||||||
const callBackState = `/auth/${redirectType}/callback`;
|
const callBackState = `/auth/${redirectType}/callback`;
|
||||||
const callbackUrl = `${config.app.urlApi}${callBackState}`;
|
const callbackUrl = `${config.app.urlApi}${callBackState}`;
|
||||||
@ -159,41 +171,35 @@ export const loginTokenRouteHandler = async (req, res, redirectType = 'web') =>
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const tokenData = {
|
||||||
const keycloakTokenData = {
|
|
||||||
access_token: response.data.access_token,
|
access_token: response.data.access_token,
|
||||||
refresh_token: response.data.refresh_token,
|
refresh_token: response.data.refresh_token,
|
||||||
id_token: response.data.id_token,
|
id_token: response.data.id_token,
|
||||||
expires_at: new Date().getTime() + response.data.expires_in * 1000,
|
expires_at: new Date().getTime() + response.data.expires_in * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const userData = await fetchAndStoreUser(keycloakTokenData);
|
req.session['keycloak-token'] = tokenData;
|
||||||
|
// Fetch and store user data, set session
|
||||||
|
const userData = await fetchAndStoreUser(req, tokenData);
|
||||||
|
const userAndTokenData = { ...tokenData, ...userData };
|
||||||
|
|
||||||
// Create Redis session with our own token
|
return userAndTokenData;
|
||||||
const { sessionToken, expiresAt } = await createSession({
|
|
||||||
user: userData,
|
|
||||||
keycloakTokens: keycloakTokenData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return our session token to client (UI expects access_token, expires_at, user)
|
|
||||||
return {
|
|
||||||
access_token: sessionToken,
|
|
||||||
expires_at: expiresAt,
|
|
||||||
...userData,
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
loginTokenRequests.set(code, tokenPromise);
|
loginTokenRequests.set(code, tokenPromise);
|
||||||
const userAndTokenData = await tokenPromise;
|
const userAndTokenData = await tokenPromise;
|
||||||
res.status(200).json(userAndTokenData);
|
res.status(200).json(userAndTokenData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.response?.data?.error_description || err.message;
|
var error = err?.response?.data?.error_description || err.message;
|
||||||
res.status(err?.response?.status || 500).json({ error: error });
|
res.status(err?.status || 500).json({ error: error });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login callback - redirect to client with auth code
|
// Login callback handler
|
||||||
export const loginCallbackRouteHandler = async (req, res, redirectType = 'web') => {
|
export const loginCallbackRouteHandler = async (req, res, redirectType = 'web') => {
|
||||||
|
// Don't use keycloak.protect() here as it expects an already authenticated session
|
||||||
|
|
||||||
|
// Extract the code and state from the query parameters
|
||||||
const code = req.query.code;
|
const code = req.query.code;
|
||||||
const state = req.query.state || '/production/overview';
|
const state = req.query.state || '/production/overview';
|
||||||
|
|
||||||
@ -201,7 +207,7 @@ export const loginCallbackRouteHandler = async (req, res, redirectType = 'web')
|
|||||||
return res.status(400).send('Authorization code missing');
|
return res.status(400).send('Authorization code missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
let appUrl;
|
var appUrl;
|
||||||
switch (redirectType) {
|
switch (redirectType) {
|
||||||
case 'web':
|
case 'web':
|
||||||
appUrl = config.app.urlClient;
|
appUrl = config.app.urlClient;
|
||||||
@ -216,22 +222,25 @@ export const loginCallbackRouteHandler = async (req, res, redirectType = 'web')
|
|||||||
appUrl = config.app.urlClient;
|
appUrl = config.app.urlClient;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUriRaw = `${appUrl}${state}`;
|
const redirectUriRaw = `${appUrl}${state}`;
|
||||||
let redirectUri;
|
let redirectUri;
|
||||||
try {
|
try {
|
||||||
|
// Try to parse as a URL (works for http/https)
|
||||||
const url = new URL(redirectUriRaw);
|
const url = new URL(redirectUriRaw);
|
||||||
url.searchParams.set('authCode', code);
|
url.searchParams.set('authCode', code);
|
||||||
redirectUri = url.toString();
|
redirectUri = url.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Fallback for custom schemes (e.g., farmcontrol://app)
|
||||||
if (redirectUriRaw.includes('?')) {
|
if (redirectUriRaw.includes('?')) {
|
||||||
redirectUri = `${redirectUriRaw}&authCode=${encodeURIComponent(code)}`;
|
redirectUri = `${redirectUriRaw}&authCode=${encodeURIComponent(code)}`;
|
||||||
} else {
|
} else {
|
||||||
redirectUri = `${redirectUriRaw}?authCode=${encodeURIComponent(code)}`;
|
redirectUri = `${redirectUriRaw}?authCode=${encodeURIComponent(code)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Save session and redirect to the original URL
|
||||||
if (redirectType === 'app-scheme') {
|
req.session.save(async () => {
|
||||||
|
if (redirectType == 'app-scheme') {
|
||||||
|
// Read HTML template and inject redirectUri
|
||||||
const templatePath = resolve(process.cwd(), 'src/services/misc/applaunch.html');
|
const templatePath = resolve(process.cwd(), 'src/services/misc/applaunch.html');
|
||||||
let html = readFileSync(templatePath, 'utf8');
|
let html = readFileSync(templatePath, 'utf8');
|
||||||
html = html.replace('__REDIRECT_URI__', redirectUri);
|
html = html.replace('__REDIRECT_URI__', redirectUri);
|
||||||
@ -239,14 +248,19 @@ export const loginCallbackRouteHandler = async (req, res, redirectType = 'web')
|
|||||||
} else {
|
} else {
|
||||||
res.redirect(redirectUri);
|
res.redirect(redirectUri);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to create or update user
|
||||||
const createOrUpdateUser = async (userInfo) => {
|
const createOrUpdateUser = async (userInfo) => {
|
||||||
try {
|
try {
|
||||||
const { username, email, name, firstName, lastName } = userInfo;
|
const { username, email, name, firstName, lastName } = userInfo;
|
||||||
|
|
||||||
|
// Find existing user by username
|
||||||
const existingUser = await userModel.findOne({ username });
|
const existingUser = await userModel.findOne({ username });
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
|
// Check if any values have changed
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
existingUser.email !== email ||
|
existingUser.email !== email ||
|
||||||
existingUser.name !== name ||
|
existingUser.name !== name ||
|
||||||
@ -254,23 +268,24 @@ const createOrUpdateUser = async (userInfo) => {
|
|||||||
existingUser.lastName !== lastName;
|
existingUser.lastName !== lastName;
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
await userModel.updateOne(
|
// Update existing user only if there are changes
|
||||||
{ username },
|
const updateData = {
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
};
|
||||||
}
|
|
||||||
);
|
await userModel.updateOne({ username }, { $set: updateData });
|
||||||
|
|
||||||
|
// Fetch the updated user to return
|
||||||
return await userModel.findOne({ username });
|
return await userModel.findOne({ username });
|
||||||
}
|
}
|
||||||
return existingUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return existingUser;
|
||||||
|
} else {
|
||||||
|
// Create new user
|
||||||
const newUser = new userModel({
|
const newUser = new userModel({
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
@ -278,8 +293,10 @@ const createOrUpdateUser = async (userInfo) => {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newUser.save();
|
await newUser.save();
|
||||||
return newUser;
|
return newUser;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating/updating user:', error);
|
logger.error('Error creating/updating user:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -287,98 +304,65 @@ const createOrUpdateUser = async (userInfo) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const userRouteHandler = (req, res) => {
|
export const userRouteHandler = (req, res) => {
|
||||||
if (req.user) {
|
if (req.session && req.session.user) {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
res.json(req.session.user);
|
||||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
} else {
|
||||||
return res.json({
|
|
||||||
access_token: token,
|
|
||||||
expires_at: req.session?.expiresAt,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.status(401).json({ error: 'Not authenticated' });
|
res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logout - delete session from Redis, redirect to Keycloak logout
|
// Logout handler
|
||||||
export const logoutRouteHandler = async (req, res) => {
|
export const logoutRouteHandler = (req, res) => {
|
||||||
|
// Get the redirect URL from query or default to login page
|
||||||
const redirectUrl = req.query.redirect_uri || '/login';
|
const redirectUrl = req.query.redirect_uri || '/login';
|
||||||
|
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
// Destroy the session
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
req.session.destroy((err) => {
|
||||||
const token = authHeader.substring(7);
|
if (err) {
|
||||||
try {
|
logger.error('Error destroying session:', err);
|
||||||
await deleteSession(token);
|
return res.status(500).json({ error: 'Failed to logout' });
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error deleting session:', err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct the Keycloak logout URL with the redirect URI
|
||||||
const logoutUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/logout`;
|
const logoutUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/logout`;
|
||||||
const encodedRedirectUri = encodeURIComponent(`${config.app.urlClient}${redirectUrl}`);
|
const encodedRedirectUri = encodeURIComponent(`${config.app.urlClient}${redirectUrl}`);
|
||||||
|
|
||||||
|
// Redirect to Keycloak logout with the redirect URI
|
||||||
res.redirect(
|
res.redirect(
|
||||||
`${logoutUrl}?client_id=${config.auth.keycloak.clientId}&post_logout_redirect_uri=${encodedRedirectUri}`
|
`${logoutUrl}?client_id=${config.auth.keycloak.clientId}&post_logout_redirect_uri=${encodedRedirectUri}`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middleware: require valid session token
|
// Token validation - protected route middleware
|
||||||
export const validateTokenMiddleware = async (req, res, next) => {
|
export const validateTokenMiddleware = keycloak.protect();
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
||||||
logger.debug('No auth header or not bearer token');
|
|
||||||
return res.status(401).json({ error: 'Not authenticated', code: 'UNAUTHORIZED' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
// Check if user has a specific role
|
||||||
const session = await getSession(token);
|
|
||||||
if (!session) {
|
|
||||||
logger.debug('Session not found');
|
|
||||||
return res.status(401).json({ error: 'Session invalid or expired', code: 'UNAUTHORIZED' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = session.user;
|
|
||||||
req.session = session;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Middleware: require specific role
|
|
||||||
export const hasRole = (role) => {
|
export const hasRole = (role) => {
|
||||||
return async (req, res, next) => {
|
return keycloak.protect((token) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
return token && token.hasRole(role);
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
});
|
||||||
return res.status(401).json({ error: 'Not authenticated', code: 'UNAUTHORIZED' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
|
||||||
const session = await getSession(token);
|
|
||||||
if (!session) {
|
|
||||||
return res.status(401).json({ error: 'Session invalid or expired', code: 'UNAUTHORIZED' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = session.user?.roles || [];
|
|
||||||
if (!roles.includes(role)) {
|
|
||||||
return res.status(403).json({ error: 'Forbidden', code: 'FORBIDDEN' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = session.user;
|
|
||||||
req.session = session;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get user info from the token
|
||||||
export const getUserInfoHandler = (req, res) => {
|
export const getUserInfoHandler = (req, res) => {
|
||||||
if (req.user) {
|
if (req.kauth && req.kauth.grant) {
|
||||||
|
const token = req.kauth.grant.access_token;
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
id: req.user._id,
|
id: token.content.sub,
|
||||||
email: req.user.email,
|
email: token.content.email,
|
||||||
name: req.user.name || `${req.user.firstName || ''} ${req.user.lastName || ''}`.trim(),
|
name:
|
||||||
roles: req.user.roles || [],
|
token.content.name ||
|
||||||
|
`${token.content.given_name || ''} ${token.content.family_name || ''}`.trim(),
|
||||||
|
roles: token.content.realm_access?.roles || [],
|
||||||
};
|
};
|
||||||
return res.json(userInfo);
|
return res.json(userInfo);
|
||||||
}
|
}
|
||||||
res.status(401).json({ error: 'Not authenticated' });
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Register route - Since we're using Keycloak, registration should be handled there
|
||||||
|
// This endpoint will redirect to Keycloak's registration page
|
||||||
export const registerRouteHandler = (req, res) => {
|
export const registerRouteHandler = (req, res) => {
|
||||||
const registrationUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/registrations`;
|
const registrationUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/registrations`;
|
||||||
const redirectUri = encodeURIComponent(config.app.urlClient + '/auth/login');
|
const redirectUri = encodeURIComponent(config.app.urlClient + '/auth/login');
|
||||||
@ -388,7 +372,8 @@ export const registerRouteHandler = (req, res) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const forgotPasswordRouteHandler = (req, res, _email) => {
|
// Forgot password handler - redirect to Keycloak's reset password page
|
||||||
|
export const forgotPasswordRouteHandler = (req, res) => {
|
||||||
const resetUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/login-actions/reset-credentials`;
|
const resetUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/login-actions/reset-credentials`;
|
||||||
const redirectUri = encodeURIComponent(config.app.urlClient + '/auth/login');
|
const redirectUri = encodeURIComponent(config.app.urlClient + '/auth/login');
|
||||||
|
|
||||||
@ -397,80 +382,76 @@ export const forgotPasswordRouteHandler = (req, res, _email) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh token - use Bearer token to find session, refresh via Keycloak, update Redis
|
// Refresh token handler
|
||||||
export const refreshTokenRouteHandler = async (req, res) => {
|
export const refreshTokenRouteHandler = (req, res) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
if (
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
!req.session ||
|
||||||
return res.status(401).json({ error: 'No session token provided' });
|
!req.session['keycloak-token'] ||
|
||||||
}
|
!req.session['keycloak-token'].refresh_token
|
||||||
|
) {
|
||||||
const sessionToken = authHeader.substring(7);
|
|
||||||
const session = await getSession(sessionToken);
|
|
||||||
if (!session || !session.keycloakTokens?.refresh_token) {
|
|
||||||
return res.status(401).json({ error: 'No refresh token available' });
|
return res.status(401).json({ error: 'No refresh token available' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshToken = req.session['keycloak-token'].refresh_token;
|
||||||
const tokenUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/token`;
|
const tokenUrl = `${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/token`;
|
||||||
|
|
||||||
try {
|
axios
|
||||||
const response = await axios.post(
|
.post(
|
||||||
tokenUrl,
|
tokenUrl,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
client_id: config.auth.keycloak.clientId,
|
client_id: config.auth.keycloak.clientId,
|
||||||
client_secret: config.auth.keycloak.clientSecret,
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
refresh_token: session.keycloakTokens.refresh_token,
|
refresh_token: refreshToken,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
.then((response) => {
|
||||||
const keycloakTokenData = {
|
// Update session with new tokens
|
||||||
|
req.session['keycloak-token'] = {
|
||||||
|
...req.session['keycloak-token'],
|
||||||
access_token: response.data.access_token,
|
access_token: response.data.access_token,
|
||||||
refresh_token: response.data.refresh_token,
|
refresh_token: response.data.refresh_token,
|
||||||
id_token: response.data.id_token,
|
|
||||||
expires_at: new Date().getTime() + response.data.expires_in * 1000,
|
expires_at: new Date().getTime() + response.data.expires_in * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateSessionKeycloakTokens(sessionToken, keycloakTokenData);
|
// Save session and return new token info
|
||||||
|
req.session.save(() => {
|
||||||
res.json({
|
res.json({
|
||||||
access_token: sessionToken,
|
access_token: response.data.access_token,
|
||||||
expires_at: keycloakTokenData.expires_at,
|
expires_at: req.session['keycloak-token'].expires_at,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
logger.error('Token refresh error:', error.response?.data || error.message);
|
logger.error('Token refresh error:', error.response?.data || error.message);
|
||||||
|
|
||||||
|
// If refresh token is invalid, clear the session
|
||||||
if (error.response?.status === 400) {
|
if (error.response?.status === 400) {
|
||||||
await deleteSession(sessionToken);
|
req.session.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(500).json({ error: 'Failed to refresh token' });
|
res.status(500).json({ error: 'Failed to refresh token' });
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middleware to populate req.user from Bearer token (Redis session or email-render JWT)
|
// Middleware to populate req.user from session or token
|
||||||
export const populateUserMiddleware = async (req, res, next) => {
|
export const populateUserMiddleware = async (req, res, next) => {
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Try Redis session first
|
// Use token-based cache to lookup user
|
||||||
const session = await getSession(token);
|
const user = await lookupUserByToken(token);
|
||||||
if (session) {
|
|
||||||
req.user = session.user;
|
|
||||||
req.session = session;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try email-render JWT (short-lived)
|
|
||||||
const user = await lookupUserByEmailRenderToken(token);
|
|
||||||
if (user) {
|
if (user) {
|
||||||
req.user = user;
|
req.user = user;
|
||||||
|
// Also set session user for compatibility
|
||||||
|
req.session.user = user;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -478,14 +459,40 @@ export const populateUserMiddleware = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to session-based authentication
|
||||||
|
if (req.session && req.session.user) {
|
||||||
|
req.user = req.session.user;
|
||||||
|
} else {
|
||||||
req.user = null;
|
req.user = null;
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
// Export cache management functions
|
||||||
lookupUserByEmailRenderToken as lookupUserByToken,
|
export { lookupUserByToken, clearTokenUserCache, getTokenUserCacheStats, removeUserFromTokenCache };
|
||||||
clearTokenUserCache,
|
|
||||||
getTokenUserCacheStats,
|
// Example of how to set up your routes in Express
|
||||||
removeUserFromTokenCache,
|
/*
|
||||||
getSession,
|
import express from "express";
|
||||||
};
|
const app = express();
|
||||||
|
|
||||||
|
// Apply session middleware
|
||||||
|
app.use(sessionMiddleware);
|
||||||
|
|
||||||
|
// Initialize Keycloak middleware
|
||||||
|
app.use(keycloak.middleware());
|
||||||
|
|
||||||
|
// Set up routes
|
||||||
|
app.get('/auth/login', loginRouteHandler);
|
||||||
|
app.get('/auth/logout', logoutRouteHandler);
|
||||||
|
app.get('/auth/register', registerRouteHandler);
|
||||||
|
app.get('/auth/forgot-password', forgotPasswordRouteHandler);
|
||||||
|
|
||||||
|
// Protected route example
|
||||||
|
app.get('/api/profile', validateTokenMiddleware, getUserInfoHandler);
|
||||||
|
|
||||||
|
// Admin-only route example
|
||||||
|
app.get('/api/admin', hasRole('admin'), (req, res) => {
|
||||||
|
res.json({ message: 'Admin access granted' });
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
/**
|
|
||||||
* Temporary 30-second auth for email render (Puppeteer).
|
|
||||||
* Isolated from auth.js to avoid circular dependency with utils.js -> auth.js -> keycloak.js -> database.js -> utils.js
|
|
||||||
*/
|
|
||||||
import config from '../../config.js';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import NodeCache from 'node-cache';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('EmailRenderAuth');
|
|
||||||
logger.level = config.server?.logLevel || 'info';
|
|
||||||
|
|
||||||
const EMAIL_RENDER_TTL = 30;
|
|
||||||
const emailRenderAuthCache = new NodeCache({ stdTTL: EMAIL_RENDER_TTL });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a temporary auth code for email render (Puppeteer) with 30-second TTL.
|
|
||||||
* The UI exchanges this code via getLoginToken to establish a brief session for rendering.
|
|
||||||
* @param {Object} userDoc - User document (must have username, email, _id, etc.)
|
|
||||||
* @returns {string} authCode to pass in URL query params
|
|
||||||
*/
|
|
||||||
export const createEmailRenderAuthCode = (userDoc) => {
|
|
||||||
const authCode = crypto.randomBytes(32).toString('hex');
|
|
||||||
const expiresAt = Date.now() + EMAIL_RENDER_TTL * 1000;
|
|
||||||
const accessToken = jwt.sign(
|
|
||||||
{ preferred_username: userDoc.username },
|
|
||||||
config.auth.sessionSecret,
|
|
||||||
{ expiresIn: EMAIL_RENDER_TTL }
|
|
||||||
);
|
|
||||||
const tokenData = {
|
|
||||||
access_token: accessToken,
|
|
||||||
expires_at: expiresAt,
|
|
||||||
_id: userDoc._id,
|
|
||||||
username: userDoc.username,
|
|
||||||
email: userDoc.email,
|
|
||||||
name: userDoc.name,
|
|
||||||
firstName: userDoc.firstName,
|
|
||||||
lastName: userDoc.lastName,
|
|
||||||
};
|
|
||||||
emailRenderAuthCache.set(authCode, tokenData);
|
|
||||||
logger.debug(`Created email render auth code (TTL ${EMAIL_RENDER_TTL}s) for user ${userDoc.username}`);
|
|
||||||
return authCode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchanges an email render auth code for token data. Consumes the code (one-time use).
|
|
||||||
* @param {string} code - The auth code from URL query
|
|
||||||
* @returns {Object|null} Token data or null if invalid/expired
|
|
||||||
*/
|
|
||||||
export const getAndConsumeEmailRenderTokenData = (code) => {
|
|
||||||
const data = emailRenderAuthCache.get(code);
|
|
||||||
if (data) {
|
|
||||||
emailRenderAuthCache.del(code);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@ -13,7 +13,6 @@ import {
|
|||||||
getModelHistory,
|
getModelHistory,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { newNoteNotification } from '../../utils.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Notes');
|
const logger = log4js.getLogger('Notes');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -128,9 +127,6 @@ export const newNoteRouteHandler = async (req, res) => {
|
|||||||
newData,
|
newData,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newNoteNotification({ ...result, ...newData }, req.user);
|
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
logger.error('No note created:', result.error);
|
logger.error('No note created:', result.error);
|
||||||
return res.status(result.code).send(result);
|
return res.status(result.code).send(result);
|
||||||
|
|||||||
@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* Redis-backed session store.
|
|
||||||
* Sessions are created after Keycloak authentication. We generate our own session tokens
|
|
||||||
* and use Redis as the source of truth. Keycloak tokens are stored for refresh.
|
|
||||||
*/
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import config from '../../config.js';
|
|
||||||
import { redisServer } from '../../database/redis.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('SessionStore');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
const SESSION_KEY_PREFIX = 'session:';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a cryptographically secure session token
|
|
||||||
*/
|
|
||||||
function generateSessionToken() {
|
|
||||||
return crypto.randomBytes(32).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get TTL in seconds from expiresAt timestamp
|
|
||||||
*/
|
|
||||||
function getTtlSeconds(expiresAt) {
|
|
||||||
const now = Date.now();
|
|
||||||
const ttlMs = expiresAt - now;
|
|
||||||
return Math.max(Math.ceil(ttlMs / 1000), 60); // minimum 60 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new session in Redis after Keycloak authentication
|
|
||||||
* @param {Object} params
|
|
||||||
* @param {Object} params.user - User object (from createOrUpdateUser)
|
|
||||||
* @param {Object} params.keycloakTokens - { access_token, refresh_token, id_token, expires_at }
|
|
||||||
* @returns {{ sessionToken: string, expiresAt: number }}
|
|
||||||
*/
|
|
||||||
export async function createSession({ user, keycloakTokens }) {
|
|
||||||
const sessionToken = generateSessionToken();
|
|
||||||
const expiresAt = keycloakTokens.expires_at;
|
|
||||||
|
|
||||||
const sessionData = {
|
|
||||||
sessionToken,
|
|
||||||
user: userToSessionUser(user),
|
|
||||||
keycloakTokens: {
|
|
||||||
access_token: keycloakTokens.access_token,
|
|
||||||
refresh_token: keycloakTokens.refresh_token,
|
|
||||||
id_token: keycloakTokens.id_token,
|
|
||||||
expires_at: keycloakTokens.expires_at,
|
|
||||||
},
|
|
||||||
expiresAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
||||||
const ttlSeconds = getTtlSeconds(expiresAt);
|
|
||||||
|
|
||||||
await redisServer.setKey(key, sessionData, ttlSeconds);
|
|
||||||
logger.debug(`Created session for user ${user.username}, expires in ${ttlSeconds}s`);
|
|
||||||
|
|
||||||
return { sessionToken, expiresAt };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get session by token. Returns null if not found or expired.
|
|
||||||
*/
|
|
||||||
export async function getSession(sessionToken) {
|
|
||||||
if (!sessionToken) return null;
|
|
||||||
|
|
||||||
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
||||||
const session = await redisServer.getKey(key);
|
|
||||||
|
|
||||||
if (!session) return null;
|
|
||||||
if (session.expiresAt && session.expiresAt <= Date.now()) {
|
|
||||||
await redisServer.deleteKey(key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update session with new Keycloak tokens (after refresh)
|
|
||||||
*/
|
|
||||||
export async function updateSessionKeycloakTokens(sessionToken, keycloakTokens) {
|
|
||||||
const session = await getSession(sessionToken);
|
|
||||||
if (!session) return null;
|
|
||||||
|
|
||||||
const updatedSession = {
|
|
||||||
...session,
|
|
||||||
keycloakTokens: {
|
|
||||||
access_token: keycloakTokens.access_token,
|
|
||||||
refresh_token: keycloakTokens.refresh_token,
|
|
||||||
id_token: keycloakTokens.id_token || session.keycloakTokens?.id_token,
|
|
||||||
expires_at: keycloakTokens.expires_at,
|
|
||||||
},
|
|
||||||
expiresAt: keycloakTokens.expires_at,
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
||||||
const ttlSeconds = getTtlSeconds(keycloakTokens.expires_at);
|
|
||||||
await redisServer.setKey(key, updatedSession, ttlSeconds);
|
|
||||||
|
|
||||||
return updatedSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a session (logout)
|
|
||||||
*/
|
|
||||||
export async function deleteSession(sessionToken) {
|
|
||||||
if (!sessionToken) return;
|
|
||||||
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
||||||
await redisServer.deleteKey(key);
|
|
||||||
logger.debug(`Deleted session for token ${sessionToken.substring(0, 12)}...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize user object for session storage (ensure _id is serializable for Redis)
|
|
||||||
*/
|
|
||||||
function userToSessionUser(user) {
|
|
||||||
if (!user) return null;
|
|
||||||
const u = { ...user };
|
|
||||||
if (u._id && typeof u._id === 'object' && u._id.toString) {
|
|
||||||
u._id = u._id;
|
|
||||||
}
|
|
||||||
return JSON.parse(JSON.stringify(u));
|
|
||||||
}
|
|
||||||
120
src/utils.js
120
src/utils.js
@ -2,7 +2,6 @@ import { mongoose } from 'mongoose';
|
|||||||
import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
|
import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
|
||||||
import { notificationModel } from './database/schemas/misc/notification.schema.js';
|
import { notificationModel } from './database/schemas/misc/notification.schema.js';
|
||||||
import { userNotifierModel } from './database/schemas/misc/usernotifier.schema.js';
|
import { userNotifierModel } from './database/schemas/misc/usernotifier.schema.js';
|
||||||
import { userModel } from './database/schemas/management/user.schema.js';
|
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { natsServer } from './database/nats.js';
|
import { natsServer } from './database/nats.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
@ -10,12 +9,6 @@ import config from './config.js';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import canonicalize from 'canonical-json';
|
import canonicalize from 'canonical-json';
|
||||||
import { getModelByName } from './services/misc/model.js';
|
import { getModelByName } from './services/misc/model.js';
|
||||||
import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
|
|
||||||
import { Worker } from 'worker_threads';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Utils');
|
const logger = log4js.getLogger('Utils');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -434,12 +427,8 @@ async function editNotification(oldValue, newValue, parentId, parentType, user)
|
|||||||
old: changedOldValues,
|
old: changedOldValues,
|
||||||
new: changedNewValues,
|
new: changedNewValues,
|
||||||
objectType: parentType,
|
objectType: parentType,
|
||||||
object: { _id: String(parentId ?? '') },
|
object: { _id: parentId },
|
||||||
user: {
|
user: { _id: user._id, firstName: user.firstName, lastName: user.lastName },
|
||||||
_id: String(user?._id ?? ''),
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -465,7 +454,7 @@ async function deleteAuditLog(deleteValue, parentId, parentType, user) {
|
|||||||
|
|
||||||
async function deleteNotification(object, parentId, parentType, user) {
|
async function deleteNotification(object, parentId, parentType, user) {
|
||||||
const model = getModelByName(parentType);
|
const model = getModelByName(parentType);
|
||||||
const objectName = object?.name || model?.label || parentType;
|
const objectName = oldValue?.name ?? newValue?.name ?? model?.label ?? parentType;
|
||||||
await notfiyObjectUserNotifiers(
|
await notfiyObjectUserNotifiers(
|
||||||
parentId,
|
parentId,
|
||||||
parentType,
|
parentType,
|
||||||
@ -481,28 +470,6 @@ async function deleteNotification(object, parentId, parentType, user) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newNoteNotification(note, user) {
|
|
||||||
const model = getModelByName(note.parentType);
|
|
||||||
const objectName = model?.label ?? note.parentType;
|
|
||||||
await notfiyObjectUserNotifiers(
|
|
||||||
note.parent,
|
|
||||||
note.parentType,
|
|
||||||
`New note added to ${objectName.toLowerCase()} by ${user?.firstName ?? 'unknown'} ${user?.lastName ?? ''}`,
|
|
||||||
`A new note has been created.`,
|
|
||||||
'newNote',
|
|
||||||
{
|
|
||||||
objectType: note.parentType,
|
|
||||||
object: { _id: String(note.parent ?? '') },
|
|
||||||
note: note,
|
|
||||||
user: {
|
|
||||||
_id: String(user?._id ?? ''),
|
|
||||||
firstName: user?.firstName,
|
|
||||||
lastName: user?.lastName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuditLogs(idOrIds) {
|
async function getAuditLogs(idOrIds) {
|
||||||
if (Array.isArray(idOrIds)) {
|
if (Array.isArray(idOrIds)) {
|
||||||
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
return auditLogModel.find({ parent: { $in: idOrIds } }).populate('owner');
|
||||||
@ -585,11 +552,6 @@ async function notfiyObjectUserNotifiers(id, objectType, title, message, type =
|
|||||||
const userNotifiers = await userNotifierModel.find({ object: id, objectType: objectType });
|
const userNotifiers = await userNotifierModel.find({ object: id, objectType: objectType });
|
||||||
for (const userNotifier of userNotifiers) {
|
for (const userNotifier of userNotifiers) {
|
||||||
await createNotification(userNotifier.user._id, title, message, type, metadata);
|
await createNotification(userNotifier.user._id, title, message, type, metadata);
|
||||||
console.log('userNotifier.email', userNotifier.email);
|
|
||||||
if (userNotifier.email == true) {
|
|
||||||
console.log('sending email');
|
|
||||||
await sendEmailNotification(userNotifier.user, title, message, type, metadata);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,79 +569,6 @@ async function createNotification(user, title, message, type = 'info', metadata)
|
|||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mailWorker = null;
|
|
||||||
|
|
||||||
function getMailWorker() {
|
|
||||||
if (!mailWorker) {
|
|
||||||
const workerPath = path.join(__dirname, 'mailworker.js');
|
|
||||||
mailWorker = new Worker(workerPath);
|
|
||||||
mailWorker.on('error', (err) => logger.error('MailWorker error:', err));
|
|
||||||
mailWorker.on('exit', (code) => {
|
|
||||||
if (code !== 0) logger.warn('MailWorker exited with code', code);
|
|
||||||
mailWorker = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return mailWorker;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an email notification asynchronously via a worker thread.
|
|
||||||
* Renders a React template on the urlClient, captures HTML with Puppeteer, and emails the user.
|
|
||||||
* Accepts the same input as createNotification: user, title, message, type, metadata.
|
|
||||||
* Returns immediately; does not wait for the email to be sent.
|
|
||||||
* @param {ObjectId|Object} user - User ID or user object (must have email)
|
|
||||||
* @param {string} title - Notification title
|
|
||||||
* @param {string} message - Notification message
|
|
||||||
* @param {string} type - Notification type (info, editObject, deleteObject, error, success)
|
|
||||||
* @param {Object} metadata - Optional metadata object
|
|
||||||
*/
|
|
||||||
async function sendEmailNotification(user, title, message, type = 'info', metadata) {
|
|
||||||
let userDoc = user;
|
|
||||||
if (user && (mongoose.Types.ObjectId.isValid(user) || user._id)) {
|
|
||||||
const userId = user._id || user;
|
|
||||||
userDoc = await userModel.findById(userId).lean();
|
|
||||||
}
|
|
||||||
if (!userDoc?.email) {
|
|
||||||
logger.warn('sendEmailNotification: no email for user', user);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const smtpConfig = config.smtp;
|
|
||||||
if (!smtpConfig?.host) {
|
|
||||||
logger.warn('sendEmailNotification: SMTP not configured, skipping email');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlClient = config.app?.urlClient || 'http://localhost:3000';
|
|
||||||
const authCode = createEmailRenderAuthCode(userDoc);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
email: userDoc.email,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
metadata: metadata || {},
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
authCode,
|
|
||||||
smtpConfig: {
|
|
||||||
host: smtpConfig.host,
|
|
||||||
port: smtpConfig.port || 587,
|
|
||||||
secure: smtpConfig.secure || false,
|
|
||||||
auth: smtpConfig.auth?.user ? smtpConfig.auth : undefined,
|
|
||||||
from: smtpConfig.from || 'FarmControl <noreply@farmcontrol.app>',
|
|
||||||
},
|
|
||||||
urlClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
getMailWorker().postMessage(payload);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('sendEmailNotification: failed to post to worker', err.message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatternObjectIds(object) {
|
function flatternObjectIds(object) {
|
||||||
if (!object || typeof object !== 'object') {
|
if (!object || typeof object !== 'object') {
|
||||||
return object;
|
return object;
|
||||||
@ -901,7 +790,6 @@ export {
|
|||||||
getAuditLogs,
|
getAuditLogs,
|
||||||
flatternObjectIds,
|
flatternObjectIds,
|
||||||
expandObjectIds,
|
expandObjectIds,
|
||||||
newNoteNotification,
|
|
||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
distributeStats,
|
distributeStats,
|
||||||
distributeNew,
|
distributeNew,
|
||||||
@ -910,8 +798,6 @@ export {
|
|||||||
distributeChildDelete,
|
distributeChildDelete,
|
||||||
distributeChildNew,
|
distributeChildNew,
|
||||||
notfiyObjectUserNotifiers,
|
notfiyObjectUserNotifiers,
|
||||||
createNotification,
|
|
||||||
sendEmailNotification,
|
|
||||||
getFilter, // <-- add here
|
getFilter, // <-- add here
|
||||||
convertPropertiesString,
|
convertPropertiesString,
|
||||||
getFileMeta,
|
getFileMeta,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user