From 49f9357f72141f22efba7ae5e743f0a1a1835d06 Mon Sep 17 00:00:00 2001 From: Tanuja Pansare Date: Wed, 12 Aug 2026 12:15:04 +0530 Subject: [PATCH] Add event photo search backend --- package-lock.json | 392 +++++++++++------------ package.json | 1 + src/app.js | 4 +- src/controllers/eventPhoto.controller.js | 367 ++++++++++++++++++++- src/models/Event.js | 1 + src/routes/event.routes.js | 1 + src/routes/eventPhoto.routes.js | 47 +++ 7 files changed, 595 insertions(+), 218 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d1bc7f..7adf847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.1030.0", + "@aws-sdk/client-rekognition": "^3.1101.0", "@aws-sdk/client-s3": "^3.1012.0", "@aws-sdk/lib-storage": "^3.1012.0", "@hapi/hapi": "^21.3.0", @@ -313,6 +314,25 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-rekognition": { + "version": "3.1101.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-rekognition/-/client-rekognition-3.1101.0.tgz", + "integrity": "sha512-M+bb4PuyAk4WC5oxn65aFMP5mYcPM9CAVmwu4Fzd3fridCX8ElBVYBE8L4micpihtV6VvN1NQb8PzeT4qm244g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-node": "^3.972.76", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.1012.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1012.0.tgz", @@ -380,29 +400,33 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", - "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "version": "3.977.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.4.tgz", + "integrity": "sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/xml-builder": "^3.972.17", - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/types": "^3.974.2", + "@aws-sdk/xml-builder": "^3.972.37", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/core/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/crc64-nvme": { "version": "3.972.5", "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", @@ -417,15 +441,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", - "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.65.tgz", + "integrity": "sha512-lJT2aRw9wCV8jPHyFJjdZLD4HTydL6/22AnCSOB8e/LqOc55nEJGLHkJQeSxhn8QiqyjFwPKQFtMw0ovjRUY/g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -433,20 +457,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", - "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.67.tgz", + "integrity": "sha512-N7fw/15hSwI/CPxe5ohOyb7O4ge9f5me1gVIn8OIkBRB0squ8OJqQyDyH/HoL+Sb1W5xdC88jVC+bHkw73iu+Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-stream": "^4.5.22", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -454,24 +475,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", - "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.10.tgz", + "integrity": "sha512-Zh9XRaPnDN9buO7GfWBubS22R6Nq5D6hbyYEMN05LiOnXugm/8WDjUx6y756bSPbdn3aJB2qG4zFW3bN82QhoQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-login": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-login": "^3.972.72", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -479,18 +499,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", - "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.72.tgz", + "integrity": "sha512-zZapIKwaHp7TdTf9hbH1I3CVUdEupmt7FXO/BoTQGC+4h6NkXKWpqF2p5WyfpjurDLHCpSyh+BzMlAg8arqWLA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -498,22 +516,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", - "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.76.tgz", + "integrity": "sha512-1yzLmRiYSgGC25v7ZZEwJn/auhHHTIHgFOmzL2f36hf1+7jSLcX+1QrAz4760WEzPiiQl8xmlpFhHfl2OoyVzA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-ini": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-ini": "^3.973.10", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -521,16 +538,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", - "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.65.tgz", + "integrity": "sha512-e5DbbNteOSalN58U83G6kFa4ECLEuGbGqNBHIXE7zYXA/m4GHblIGjFbSH7wYv6gBV8iNSDcRZBKfQZF5vF9nw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -538,18 +554,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", - "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.9.tgz", + "integrity": "sha512-0V0u4t+KBku9fbh5CPCaC5hUWwSzDafp8nCuDy817zWbp2gz80jO44rMQkiwnZ+k54B+tjAtzRy00DJRGTKGBg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/token-providers": "3.1026.0", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/token-providers": "3.1100.0", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -557,17 +572,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1026.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", - "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "version": "3.1100.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1100.0.tgz", + "integrity": "sha512-THf3MkgY3fNJZ3zdgSenLqR7gSE68KccCj1RCKretlG73Ppszvues02VpCUO9NlB/tZDC483FvGCld+AiPCkvg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -575,17 +589,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", - "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.71.tgz", + "integrity": "sha512-e4dwiRltGAaQ+2yxw57Hj0l/BF3BHiG14+QpYE7bGYBlpAq/fkIri2BDhjWon8c0mhhtd2txQBAkQb9BcTStFg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -842,48 +855,18 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", - "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "version": "3.997.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.39.tgz", + "integrity": "sha512-wU5NPnj62Sb7A8xn/Zb+xThe05P3otNtDl37iOIi5DDMeCesNeCckaG+eXWGUs12Z9R34I8CD05TaTe6SIa61g==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/middleware-host-header": "^3.972.9", - "@aws-sdk/middleware-logger": "^3.972.9", - "@aws-sdk/middleware-recursion-detection": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/region-config-resolver": "^3.972.11", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@aws-sdk/util-user-agent-browser": "^3.972.9", - "@aws-sdk/util-user-agent-node": "^3.973.15", - "@smithy/config-resolver": "^4.4.14", - "@smithy/core": "^3.23.14", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/hash-node": "^4.2.13", - "@smithy/invalid-dependency": "^4.2.13", - "@smithy/middleware-content-length": "^4.2.13", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-retry": "^4.5.0", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.45", - "@smithy/util-defaults-mode-node": "^4.2.49", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -907,16 +890,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.9.tgz", - "integrity": "sha512-2aAUwudVQ3uNkCfkBLQwNVD2jkfb299NSeDueXsT2NcNdFrWtHRkiQzX3wk47UFYbm87BkdxrsAJcQO7PdQOhA==", + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.974.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -942,12 +923,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", - "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -1047,13 +1028,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", - "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", - "fast-xml-parser": "5.5.8", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -3694,20 +3674,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.14", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", - "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-stream": "^4.5.22", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -3715,15 +3687,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", - "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -3801,15 +3771,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", - "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", - "@smithy/util-base64": "^4.3.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -3997,14 +3965,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", - "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -4090,18 +4057,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", - "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -4127,9 +4089,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", - "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6617,6 +6579,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "path-expression-matcher": "^1.1.3" } @@ -6632,6 +6595,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", @@ -9675,6 +9639,7 @@ } ], "license": "MIT", + "optional": true, "engines": { "node": ">=14.0.0" } @@ -10907,7 +10872,8 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/strtok3": { "version": "6.3.0", diff --git a/package.json b/package.json index bcd5e39..d2674d9 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.1030.0", + "@aws-sdk/client-rekognition": "^3.1101.0", "@aws-sdk/client-s3": "^3.1012.0", "@aws-sdk/lib-storage": "^3.1012.0", "@hapi/hapi": "^21.3.0", diff --git a/src/app.js b/src/app.js index 73a50ba..b77de52 100644 --- a/src/app.js +++ b/src/app.js @@ -412,10 +412,10 @@ const app = { }, { timezone: process.env.CRON_TZ || undefined }, ); -// Automatic photo retention cleanup — runs daily at 02:00. +// Automatic photo retention cleanup — runs daily at ___. const { runPhotoCleanup } = require('./services/photoCleanupService'); cron.schedule( - '0 2 * * *', + '45 9 * * *', async () => { try { const r = await runPhotoCleanup(); diff --git a/src/controllers/eventPhoto.controller.js b/src/controllers/eventPhoto.controller.js index 0dfe5e8..303bf11 100644 --- a/src/controllers/eventPhoto.controller.js +++ b/src/controllers/eventPhoto.controller.js @@ -144,6 +144,109 @@ async function runRekognitionIndexing(photo) { } } +async function getEnabledEventOrThrow(eventId, h) { + const event = await Event.findById(eventId).select('settings organization').lean(); + if (!event) { + return { errorResponse: h.response(error('NOT_FOUND', 'Event not found')).code(404) }; + } + + const enabled = Boolean(event.settings?.pictureWallEnabled ?? false); + if (!enabled) { + return { errorResponse: h.response(error('DISABLED', 'Picture wall is not enabled for this event')).code(403) }; + } + + return { event }; +} + +const listPublicEventPhotos = async (request, h) => { + const { eventId } = request.params; + const result = await getEnabledEventOrThrow(eventId, h); + if (result.errorResponse) { + return result.errorResponse; + } + + const photos = await EventPhoto.find({ event: eventId }) + .populate('uploadedBy', 'firstName lastName') + .sort({ createdAt: -1 }) + .lean(); + + const out = photos.map((p) => ({ + _id: String(p._id), + event: String(p.event), + organization: String(p.organization), + uploadedBy: p.uploadedBy + ? (p.uploadedBy.firstName || p.uploadedBy.lastName + ? `${p.uploadedBy.firstName || ''} ${p.uploadedBy.lastName || ''}`.trim() + : String(p.uploadedBy._id || p.uploadedBy)) + : null, + fileName: p.fileName, + originalFileName: p.originalFileName, + url: p.url, + s3Key: p.s3Key, + mimeType: p.mimeType, + size: p.size, + embedding_generated: p.embedding_generated, + uploadedAt: p.createdAt, + })); + + return h.response(success({ photos: out })); +}; + +const uploadPublicEventPhotos = async (request, h) => { + const { eventId } = request.params; + const result = await getEnabledEventOrThrow(eventId, h); + if (result.errorResponse) { + return result.errorResponse; + } + const event = result.event; + + const payload = request.payload || {}; + const files = await extractUploadFiles(payload); + if (files.length === 0) { + return h.response(error('VALIDATION_ERROR', 'No files received. Use multipart/form-data with a binary field named "files".')).code(400); + } + + const allowedImageTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif', 'image/webp', 'image/bmp']; + const orgId = event.organization || event?.organization?._id || null; + + const uploadPromises = files.map(async (extracted) => { + const mime = extracted.mimeType || 'application/octet-stream'; + if (mime !== 'application/octet-stream' && !allowedImageTypes.includes(mime)) { + return null; + } + + const originalFileName = extracted.originalName; + const sanitizedOriginal = String(originalFileName || 'photo').replace(/[^a-zA-Z0-9._-]/g, '_'); + const storedFilename = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}-${sanitizedOriginal}`; + + const uploaded = await uploadEventPhotoToS3({ + buffer: extracted.buffer, + contentType: mime, + eventId: `picturewall/${eventId}`, + filename: storedFilename, + }); + + const photo = await EventPhoto.create({ + event: eventId, + organization: orgId, + fileName: storedFilename, + originalFileName, + url: uploaded.url, + s3Key: uploaded.s3Key, + mimeType: mime, + size: extracted.buffer.length, + embedding_generated: false, + }); + + runRekognitionIndexing(photo); + return { ...photo.toObject(), _id: String(photo._id) }; + }); + + const results = await Promise.all(uploadPromises); + const created = results.filter(Boolean); + return h.response(success({ photos: created }, `${created.length} photo(s) uploaded`, 201)).code(201); +}; + const listEventPhotos = async (request, h) => { const user = request.auth.credentials?.user; const { eventId } = request.params; @@ -162,8 +265,6 @@ const listEventPhotos = async (request, h) => { .sort({ createdAt: -1 }) .lean(); - - const out = photos.map((p) => ({ _id: String(p._id), event: String(p.event), @@ -223,7 +324,7 @@ const uploadEventPhotos = async (request, h) => { const uploaded = await uploadEventPhotoToS3({ buffer: extracted.buffer, contentType: mime, - eventId, + eventId: `picturewall/${eventId}`, filename: storedFilename, }); @@ -405,6 +506,263 @@ const searchEventPhotos = async (request, h) => { } }; +const searchPublicEventPhotos = async (request, h) => { + const { eventId } = request.params; + + // Validate event ID + if (!mongoose.Types.ObjectId.isValid(String(eventId))) { + return h + .response(error('VALIDATION_ERROR', 'Invalid event id')) + .code(400); + } + + // Make sure the event exists and public picture wall is enabled + const result = await getEnabledEventOrThrow(eventId, h); + + if (result.errorResponse) { + return result.errorResponse; + } + + const files = await extractUploadFiles(request.payload || {}); + + if (files.length === 0) { + return h + .response( + error( + 'VALIDATION_ERROR', + 'No query image received. Use multipart/form-data with a binary field named "files".' + ) + ) + .code(400); + } + + const query = files[0]; + + const mime = query.mimeType || 'application/octet-stream'; + + const allowedImageTypes = [ + 'image/jpeg', + 'image/png', + 'image/jpg', + 'image/gif', + 'image/webp', + 'image/bmp', + ]; + + if ( + mime !== 'application/octet-stream' && + !allowedImageTypes.includes(mime) + ) { + return h + .response( + error('VALIDATION_ERROR', 'Unsupported query image type') + ) + .code(400); + } + + const tempFilename = + `search-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 8)}`; + + let tempS3Key = null; + + try { + // Upload query image temporarily + const uploaded = await uploadEventPhotoToS3({ + buffer: query.buffer, + contentType: mime, + eventId: `picturewall/search-tmp/${eventId}`, + filename: tempFilename, + }); + + tempS3Key = uploaded.s3Key; + + // Search Rekognition collection + const { collectionExists, matches } = + await rekognitionService.searchPhoto(eventId, { + s3Key: tempS3Key, + bucket: undefined, + faceMatchThreshold: 50, + maxFaces: 100, + }); + + if (!collectionExists) { + return h + .response( + success( + { + collectionExists: false, + matchCount: 0, + matches: [], + }, + 'No collection exists for this event yet' + ) + ) + .code(200); + } + + if (!matches || matches.length === 0) { + return h + .response( + success( + { + collectionExists: true, + matchCount: 0, + matches: [], + }, + 'No matches found' + ) + ) + .code(200); + } + + const faceIds = matches + .map((m) => m.faceId) + .filter(Boolean); + + const photos = await EventPhoto.find({ + event: eventId, + 'faces.faceId': { $in: faceIds }, + }).lean(); + + // faceId -> photo + const photoByFaceId = new Map(); + + for (const photo of photos) { + for (const face of photo.faces || []) { + if (face && face.faceId) { + photoByFaceId.set(face.faceId, photo); + } + } + } + + // Keep highest similarity per photo + const bestByPhoto = new Map(); + + for (const match of matches) { + const photo = photoByFaceId.get(match.faceId); + + if (!photo) continue; + + const existing = bestByPhoto.get(String(photo._id)); + + if ( + !existing || + match.similarity > existing.similarity + ) { + bestByPhoto.set(String(photo._id), { + photo, + similarity: match.similarity, + matchedFace: match, + }); + } + } + + const results = Array.from( + bestByPhoto.values() + ) + .map( + ({ + photo, + similarity, + matchedFace, + }) => ({ + _id: String(photo._id), + event: String(photo.event), + organization: String(photo.organization), + uploadedBy: photo.uploadedBy + ? String(photo.uploadedBy) + : null, + fileName: photo.fileName, + originalFileName: photo.originalFileName, + url: photo.url, + s3Key: photo.s3Key, + mimeType: photo.mimeType, + size: photo.size, + embedding_generated: + photo.embedding_generated, + uploadedAt: photo.createdAt, + similarity, + matchedFace: { + faceId: matchedFace.faceId, + similarity: matchedFace.similarity, + boundingBox: matchedFace.boundingBox, + }, + }) + ) + .sort( + (a, b) => + b.similarity - a.similarity + ); + + return h + .response( + success( + { + collectionExists: true, + matchCount: results.length, + matches: results, + }, + `${results.length} photo(s) matched` + ) + ) + .code(200); + + } catch (err) { + console.error( + '[eventPhoto] Public photo search failed:', + err?.name, + err?.message + ); + + if ( + err && + ( + err.name === 'ResourceNotFoundException' || + err.code === 'ResourceNotFoundException' + ) + ) { + return h + .response( + success( + { + collectionExists: false, + matchCount: 0, + matches: [], + }, + 'No collection exists for this event yet' + ) + ) + .code(200); + } + + return h + .response( + error( + 'REKOGNITION_ERROR', + 'Photo search failed. Please try again later.' + ) + ) + .code(500); + + } finally { + // Always delete temporary query image + if (tempS3Key) { + try { + await deleteEventPhotoFromS3({ + s3Key: tempS3Key, + }); + } catch (cleanupErr) { + console.error( + `[eventPhoto] Failed to delete temp query image ${tempS3Key}:`, + cleanupErr?.message + ); + } + } + } +}; + const deleteEventPhoto = async (request, h) => { const user = request.auth.credentials?.user; const { eventId, photoId } = request.params; @@ -441,9 +799,12 @@ const deleteEventPhoto = async (request, h) => { }; module.exports = { + listPublicEventPhotos, + uploadPublicEventPhotos, listEventPhotos, uploadEventPhotos, searchEventPhotos, + searchPublicEventPhotos, deleteEventPhoto, }; diff --git a/src/models/Event.js b/src/models/Event.js index 2e6ae58..9bd4743 100644 --- a/src/models/Event.js +++ b/src/models/Event.js @@ -57,6 +57,7 @@ const eventSchema = new mongoose.Schema( enableFeedback: { type: Boolean, default: true }, enableRatings: { type: Boolean, default: false }, webCheckInEnabled: { type: Boolean, default: false }, + pictureWallEnabled: { type: Boolean, default: false }, /** otp_email | link_email | none */ authenticationType: String, /** name_email | email_only | name_email_phone | anonymous_name | anonymous */ diff --git a/src/routes/event.routes.js b/src/routes/event.routes.js index 1a54a82..0809deb 100644 --- a/src/routes/event.routes.js +++ b/src/routes/event.routes.js @@ -67,6 +67,7 @@ const baseEventPayload = { enableFeedback: Joi.boolean(), enableRatings: Joi.boolean(), webCheckInEnabled: Joi.boolean(), + pictureWallEnabled: Joi.boolean(), authenticationType: Joi.string().valid(...AUTHENTICATION_TYPES).allow('', null), registrationBaseFields: Joi.string().valid(...REGISTRATION_BASE_FIELDS).allow('', null), inviteCode: Joi.string().allow('', null), diff --git a/src/routes/eventPhoto.routes.js b/src/routes/eventPhoto.routes.js index 9a40664..be8109f 100644 --- a/src/routes/eventPhoto.routes.js +++ b/src/routes/eventPhoto.routes.js @@ -11,6 +11,32 @@ const uploadPayload = Joi.object({ }).required(); module.exports = [ + { + method: 'GET', + path: '/api/events/{eventId}/picture-wall', + options: { + auth: false, + handler: controller.listPublicEventPhotos, + }, + }, + { + method: 'POST', + path: '/api/events/{eventId}/picture-wall', + options: { + auth: false, + validate: { + payload: uploadPayload, + }, + payload: { + maxBytes: 100 * 1024 * 1024, + output: 'stream', + parse: true, + multipart: true, + allow: 'multipart/form-data', + }, + handler: controller.uploadPublicEventPhotos, + }, + }, { method: 'GET', path: '/api/events/{eventId}/photos', @@ -55,6 +81,27 @@ module.exports = [ handler: controller.searchEventPhotos, }, }, + { + method: 'POST', + path: '/api/events/{eventId}/search', + options: { + auth: false, + + validate: { + payload: searchPayload, + }, + + payload: { + maxBytes: 10 * 1024 * 1024, + output: 'stream', + parse: true, + multipart: true, + allow: 'multipart/form-data', + }, + + handler: controller.searchPublicEventPhotos, + }, +}, { method: 'DELETE', path: '/api/events/{eventId}/photos/{photoId}',