From 93f839afe1105c1e2a1863e8161d77dd0d18484c Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Thu, 12 Feb 2026 17:35:33 -0500 Subject: [PATCH 1/6] install aws packages --- .../lambdas/expenditures/package-lock.json | 490 +++++++++++++++++- .../backend/lambdas/expenditures/package.json | 2 + 2 files changed, 479 insertions(+), 13 deletions(-) diff --git a/apps/backend/lambdas/expenditures/package-lock.json b/apps/backend/lambdas/expenditures/package-lock.json index cdf4bb3..c98be8c 100644 --- a/apps/backend/lambdas/expenditures/package-lock.json +++ b/apps/backend/lambdas/expenditures/package-lock.json @@ -8,6 +8,8 @@ "name": "lambda-local", "version": "1.0.0", "dependencies": { + "aws-jwt-verify": "^5.1.1", + "aws-lambda": "^1.0.7", "jest": "^30.2.0", "kysely": "^0.28.8", "pg": "^8.16.3" @@ -1614,6 +1616,89 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-jwt-verify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-5.1.1.tgz", + "integrity": "sha512-j6whGdGJmQ27agk4ijY8RPv6itb8JLb7SCJ86fEnneTcSBrpxuwL8kLq6y5WVH95aIknyAloEqAsaOLS1J8ITQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/aws-lambda": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz", + "integrity": "sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w==", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.814.0", + "commander": "^3.0.2", + "js-yaml": "^3.14.1", + "watchpack": "^2.0.0-beta.10" + }, + "bin": { + "lambda": "bin/lambda" + } + }, + "node_modules/aws-lambda/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-lambda/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -1726,6 +1811,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.28", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", @@ -1818,17 +1923,45 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1838,6 +1971,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2051,6 +2200,12 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2124,6 +2279,23 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2157,7 +2329,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2218,7 +2389,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2228,7 +2398,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2238,7 +2407,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2310,6 +2478,15 @@ "through": "~2.3.1" } }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2426,6 +2603,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2490,12 +2682,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2518,7 +2718,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2552,7 +2751,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -2594,11 +2792,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2644,11 +2847,22 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2661,7 +2875,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -2677,7 +2890,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2701,6 +2913,12 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2746,12 +2964,40 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2770,6 +3016,25 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2779,6 +3044,24 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2791,6 +3074,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3467,6 +3771,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/joi": { "version": "18.0.1", "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", @@ -3657,7 +3970,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4104,6 +4416,15 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -4192,6 +4513,12 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -4208,6 +4535,15 @@ ], "license": "MIT" }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -4254,6 +4590,29 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4263,6 +4622,23 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4924,6 +5300,38 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -4974,6 +5382,19 @@ "makeerror": "1.0.12" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4989,6 +5410,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -5103,6 +5545,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/apps/backend/lambdas/expenditures/package.json b/apps/backend/lambdas/expenditures/package.json index dbc2306..0ac91c6 100644 --- a/apps/backend/lambdas/expenditures/package.json +++ b/apps/backend/lambdas/expenditures/package.json @@ -23,6 +23,8 @@ "typescript": "^5.4.5" }, "dependencies": { + "aws-jwt-verify": "^5.1.1", + "aws-lambda": "^1.0.7", "jest": "^30.2.0", "kysely": "^0.28.8", "pg": "^8.16.3" From 8fa1a3539442dd919060ccd4cbc085144446d40b Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Thu, 12 Feb 2026 17:36:12 -0500 Subject: [PATCH 2/6] add auth handler to expenditures lambda --- apps/backend/lambdas/expenditures/auth.ts | 189 ++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 apps/backend/lambdas/expenditures/auth.ts diff --git a/apps/backend/lambdas/expenditures/auth.ts b/apps/backend/lambdas/expenditures/auth.ts new file mode 100644 index 0000000..0fd2e84 --- /dev/null +++ b/apps/backend/lambdas/expenditures/auth.ts @@ -0,0 +1,189 @@ +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { CognitoJwtVerifier } from 'aws-jwt-verify'; +import db from './db'; + +// Load from environment variables +const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || ''; +const COGNITO_REGION = process.env.AWS_REGION || 'us-east-2'; +const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID || ''; + +// Create verifier instance lazily (only when needed) +let verifier: any = null; + +function getVerifier() { + if (!verifier) { + if (!COGNITO_USER_POOL_ID) { + throw new Error('COGNITO_USER_POOL_ID environment variable is not set'); + } + verifier = CognitoJwtVerifier.create({ + userPoolId: COGNITO_USER_POOL_ID, + tokenUse: 'access', + clientId: COGNITO_CLIENT_ID, + }); + } + return verifier; +} + +export interface AuthenticatedUser { + cognitoSub: string; + userId?: number; + email?: string; + isAdmin: boolean; + cognitoGroups?: string[]; +} + +export interface AuthContext { + user?: AuthenticatedUser; + isAuthenticated: boolean; +} + +/** + * Extract JWT token from Authorization header + */ +function extractToken(event: any): string | null { + const authHeader = event.headers?.Authorization || event.headers?.authorization; + + if (!authHeader) { + return null; + } + + const parts = authHeader.split(' '); + if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') { + return parts[1]; + } + + return authHeader; +} + +/** + * Verify and decode Cognito JWT token, then load user from database + */ +export async function authenticateRequest(event: any): Promise { + const token = extractToken(event); + + if (!token) { + return { isAuthenticated: false }; + } + + try { + const payload = await getVerifier().verify(token); + + const dbUser = await db + .selectFrom('branch.users') + .where('cognito_sub', '=', payload.sub) + .selectAll() + .executeTakeFirst(); + + if (!dbUser) { + console.warn('User authenticated with Cognito but not found in database:', payload.sub); + return { isAuthenticated: false }; + } + + const user: AuthenticatedUser = { + cognitoSub: payload.sub, + userId: dbUser.user_id, + email: payload.email as string | undefined, + isAdmin: dbUser.is_admin === true, + cognitoGroups: payload['cognito:groups'] as string[] | undefined, + }; + + if (user.cognitoGroups?.includes('Admins')) { + user.isAdmin = true; + } + + return { + user, + isAuthenticated: true, + }; + } catch (error) { + console.error('Token verification failed:', error); + return { isAuthenticated: false }; + } +} + +/** + * Authorization helpers for different access levels + */ +export type AccessLevel = 'PUBLIC' | 'AUTHENTICATED' | 'ADMIN' | 'SELF' | 'ADMIN_OR_SELF'; + +export interface AuthorizationCheck { + allowed: boolean; + reason?: string; +} + +/** + * Check if user is authorized for a given access level + * @param authContext - The authentication context + * @param requiredAccess - Required access level + * @param resourceUserId - The user_id of the resource being accessed (for SELF/ADMIN_OR_SELF checks) + */ +export function checkAuthorization( + authContext: AuthContext, + requiredAccess: AccessLevel, + resourceUserId?: number | string +): AuthorizationCheck { + if (requiredAccess === 'PUBLIC') { + return { allowed: true }; + } + + // All other access levels require authentication + if (!authContext.isAuthenticated || !authContext.user) { + return { + allowed: false, + reason: 'Authentication required' + }; + } + + const { user } = authContext; + + switch (requiredAccess) { + case 'AUTHENTICATED': + return { allowed: true }; + + case 'ADMIN': + if (!user.isAdmin) { + return { + allowed: false, + reason: 'Admin access required' + }; + } + return { allowed: true }; + + case 'SELF': + if (!resourceUserId) { + return { + allowed: false, + reason: 'Resource user ID required for SELF access check' + }; + } + if (user.userId !== Number(resourceUserId)) { + return { + allowed: false, + reason: 'Can only access own resources' + }; + } + return { allowed: true }; + + case 'ADMIN_OR_SELF': + if (!resourceUserId) { + return { + allowed: false, + reason: 'Resource user ID required for ADMIN_OR_SELF access check' + }; + } + // Admin can access anything, or user can access their own resources + if (user.isAdmin || user.userId === Number(resourceUserId)) { + return { allowed: true }; + } + return { + allowed: false, + reason: 'Admin access or resource ownership required' + }; + + default: + return { + allowed: false, + reason: 'Unknown access level' + }; + } +} \ No newline at end of file From c4ff526b518f4344a739adc185c7d90670d31c3c Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Thu, 12 Feb 2026 17:36:27 -0500 Subject: [PATCH 3/6] add cognito_sub field to branchusers --- apps/backend/lambdas/expenditures/db-types.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/lambdas/expenditures/db-types.d.ts b/apps/backend/lambdas/expenditures/db-types.d.ts index b2bc948..3d190d8 100644 --- a/apps/backend/lambdas/expenditures/db-types.d.ts +++ b/apps/backend/lambdas/expenditures/db-types.d.ts @@ -60,6 +60,7 @@ export interface BranchProjects { } export interface BranchUsers { + cognito_sub: string | null; created_at: Generated; email: string; is_admin: Generated; From 4efcacd92934536b983ecb4fb463890cea3c78b7 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 18 Feb 2026 21:38:46 -0500 Subject: [PATCH 4/6] add auth check to POST expenditures --- apps/backend/lambdas/expenditures/auth.ts | 88 ------------------- apps/backend/lambdas/expenditures/handler.ts | 48 ++++++---- .../backend/lambdas/expenditures/openapi.yaml | 12 ++- .../lambdas/expenditures/validation-utils.ts | 27 +----- 4 files changed, 39 insertions(+), 136 deletions(-) diff --git a/apps/backend/lambdas/expenditures/auth.ts b/apps/backend/lambdas/expenditures/auth.ts index 0fd2e84..5d64d0d 100644 --- a/apps/backend/lambdas/expenditures/auth.ts +++ b/apps/backend/lambdas/expenditures/auth.ts @@ -1,10 +1,8 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; import { CognitoJwtVerifier } from 'aws-jwt-verify'; import db from './db'; // Load from environment variables const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || ''; -const COGNITO_REGION = process.env.AWS_REGION || 'us-east-2'; const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID || ''; // Create verifier instance lazily (only when needed) @@ -101,89 +99,3 @@ export async function authenticateRequest(event: any): Promise { } } -/** - * Authorization helpers for different access levels - */ -export type AccessLevel = 'PUBLIC' | 'AUTHENTICATED' | 'ADMIN' | 'SELF' | 'ADMIN_OR_SELF'; - -export interface AuthorizationCheck { - allowed: boolean; - reason?: string; -} - -/** - * Check if user is authorized for a given access level - * @param authContext - The authentication context - * @param requiredAccess - Required access level - * @param resourceUserId - The user_id of the resource being accessed (for SELF/ADMIN_OR_SELF checks) - */ -export function checkAuthorization( - authContext: AuthContext, - requiredAccess: AccessLevel, - resourceUserId?: number | string -): AuthorizationCheck { - if (requiredAccess === 'PUBLIC') { - return { allowed: true }; - } - - // All other access levels require authentication - if (!authContext.isAuthenticated || !authContext.user) { - return { - allowed: false, - reason: 'Authentication required' - }; - } - - const { user } = authContext; - - switch (requiredAccess) { - case 'AUTHENTICATED': - return { allowed: true }; - - case 'ADMIN': - if (!user.isAdmin) { - return { - allowed: false, - reason: 'Admin access required' - }; - } - return { allowed: true }; - - case 'SELF': - if (!resourceUserId) { - return { - allowed: false, - reason: 'Resource user ID required for SELF access check' - }; - } - if (user.userId !== Number(resourceUserId)) { - return { - allowed: false, - reason: 'Can only access own resources' - }; - } - return { allowed: true }; - - case 'ADMIN_OR_SELF': - if (!resourceUserId) { - return { - allowed: false, - reason: 'Resource user ID required for ADMIN_OR_SELF access check' - }; - } - // Admin can access anything, or user can access their own resources - if (user.isAdmin || user.userId === Number(resourceUserId)) { - return { allowed: true }; - } - return { - allowed: false, - reason: 'Admin access or resource ownership required' - }; - - default: - return { - allowed: false, - reason: 'Unknown access level' - }; - } -} \ No newline at end of file diff --git a/apps/backend/lambdas/expenditures/handler.ts b/apps/backend/lambdas/expenditures/handler.ts index 3dc3d1e..8d00915 100644 --- a/apps/backend/lambdas/expenditures/handler.ts +++ b/apps/backend/lambdas/expenditures/handler.ts @@ -1,6 +1,7 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { APIGatewayProxyResult } from 'aws-lambda'; import db from './db'; import { ExpenditureValidationUtils } from './validation-utils'; +import { authenticateRequest } from './auth'; export const handler = async (event: any): Promise => { try { @@ -21,15 +22,37 @@ export const handler = async (event: any): Promise => { // POST /expenditures if ((normalizedPath === '/expenditures' || normalizedPath === '' || normalizedPath === '/') && method === 'POST') { + // Authenticate the request + const authContext = await authenticateRequest(event); + if (!authContext.isAuthenticated || !authContext.user) { + return json(401, { message: 'Authentication required' }); + } + + const { user } = authContext; + const body = event.body ? JSON.parse(event.body) as Record : {}; - + // Validate input const validationResult = ExpenditureValidationUtils.validateExpenditureInput(body); if (validationResult instanceof Error) { return json(400, { message: validationResult.message }); } - const { projectID, enteredBy, amount, category, description, spentOn } = validationResult; + const { projectID, amount, category, description, spentOn } = validationResult; + + // Authorize: must be global admin, or PI/Accountant/Admin on this project + if (!user.isAdmin) { + const membership = await db + .selectFrom('branch.project_memberships') + .where('project_id', '=', projectID) + .where('user_id', '=', user.userId!) + .select('role') + .executeTakeFirst(); + + if (!membership || !['PI', 'Accountant', 'Admin'].includes(membership.role)) { + return json(403, { message: 'Unable to create expenditure for this project' }); + } + } // Check if project exists const project = await db @@ -42,26 +65,13 @@ export const handler = async (event: any): Promise => { return json(404, { message: 'Project not found' }); } - // Check if enteredBy user exists (if provided) - if (enteredBy !== undefined && enteredBy !== null) { - const user = await db - .selectFrom('branch.users') - .where('user_id', '=', enteredBy) - .selectAll() - .executeTakeFirst(); - - if (!user) { - return json(404, { message: 'User not found' }); - } - } - - // Insert expenditure + // Insert expenditure with authenticated user as entered_by try { await db .insertInto('branch.expenditures') .values({ project_id: projectID, - entered_by: enteredBy ?? null, + entered_by: user.userId!, amount, category: category ?? null, description: description ?? null, @@ -78,7 +88,7 @@ export const handler = async (event: any): Promise => { route: 'POST /expenditures', body: { projectID, - enteredBy: enteredBy ?? null, + enteredBy: user.userId!, amount, category: category ?? null, description: description ?? null, diff --git a/apps/backend/lambdas/expenditures/openapi.yaml b/apps/backend/lambdas/expenditures/openapi.yaml index 09a0fba..596334d 100644 --- a/apps/backend/lambdas/expenditures/openapi.yaml +++ b/apps/backend/lambdas/expenditures/openapi.yaml @@ -28,15 +28,21 @@ paths: application/json: schema: type: object + required: + - projectID + - amount properties: - enteredBy: + projectID: type: number amount: type: number + category: + type: string description: type: string - projectID: - type: number + spentOn: + type: string + format: date responses: '201': description: Success diff --git a/apps/backend/lambdas/expenditures/validation-utils.ts b/apps/backend/lambdas/expenditures/validation-utils.ts index a62f733..4078c31 100644 --- a/apps/backend/lambdas/expenditures/validation-utils.ts +++ b/apps/backend/lambdas/expenditures/validation-utils.ts @@ -1,16 +1,9 @@ -export type ValidationResult = { - isValid: boolean; - value?: T; - error?: string; -}; - export interface ExpenditureInput { projectID: number; - enteredBy?: number; amount: number; category?: string; description?: string; - spentOn?: string; + spentOn?: string; } export class ExpenditureValidationUtils { @@ -42,18 +35,6 @@ export class ExpenditureValidationUtils { return amount; } - static validateEnteredBy(enteredBy: unknown): number | undefined | Error { - if (enteredBy === undefined || enteredBy === null) { - return undefined; - } - - if (typeof enteredBy !== 'number' || !Number.isInteger(enteredBy)) { - return new Error('enteredBy must be an integer'); - } - - return enteredBy; - } - static validateCategory(category: unknown): string | undefined | Error { if (category === undefined || category === null) { return undefined; @@ -103,11 +84,6 @@ export class ExpenditureValidationUtils { } // Validate optional fields - const enteredBy = this.validateEnteredBy(body.enteredBy); - if (enteredBy instanceof Error) { - return enteredBy; - } - const category = this.validateCategory(body.category); if (category instanceof Error) { return category; @@ -125,7 +101,6 @@ export class ExpenditureValidationUtils { return { projectID, - enteredBy, amount, category, description, From 4e4c5d0b9c582d4c4d406c276056e90568746e32 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Wed, 18 Feb 2026 21:39:00 -0500 Subject: [PATCH 5/6] update tests to handle auth checks --- .../test/expenditures.e2e.test.ts | 126 ++-------------- .../test/expenditures.unit.test.ts | 140 +++++++++++++----- 2 files changed, 119 insertions(+), 147 deletions(-) diff --git a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts index 5d999ff..a852395 100644 --- a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts +++ b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts @@ -1,6 +1,4 @@ -import { test, expect, beforeEach, afterAll } from '@jest/globals'; -import fs from 'fs'; -import path from 'path'; +import { test, expect, afterAll } from '@jest/globals'; import { Pool } from 'pg'; const pool = new Pool({ @@ -12,141 +10,45 @@ const pool = new Pool({ ssl: false, }); -const seedSqlPath = path.resolve(__dirname, '../../../db/db_setup.sql'); -const seedSql = fs.readFileSync(seedSqlPath, 'utf8'); - -beforeEach(async () => { - const client = await pool.connect(); - try { - await client.query(seedSql); - } finally { - client.release(); - } -}); - afterAll(async () => { await pool.end(); }); -test("health test 🌞", async () => { - let res = await fetch("http://localhost:3000/expenditures/health") +test("health check", async () => { + const res = await fetch("http://localhost:3000/expenditures/health"); expect(res.status).toBe(200); + const body = await res.json(); + expect(body.ok).toBe(true); }); -test("post expenditure with all fields", async () => { +test("401: unauthenticated POST is rejected", async () => { const res = await fetch("http://localhost:3000/expenditures", { method: "POST", body: JSON.stringify({ projectID: 1, - enteredBy: 1, amount: 1500.50, category: "Travel", description: "Conference flight and hotel", spentOn: "2025-08-15" }) }); - expect(res.status).toBe(201); + expect(res.status).toBe(401); const body = await res.json(); - expect(body.ok).toBe(true); - expect(body.body.projectID).toBe(1); - expect(body.body.enteredBy).toBe(1); - expect(body.body.amount).toBe(1500.50); - expect(body.body.category).toBe("Travel"); - expect(body.body.description).toBe("Conference flight and hotel"); - expect(body.body.spentOn).toBe("2025-08-15"); -}); - -test("post expenditure with only required fields", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 2, - amount: 2000 - }) - }); - expect(res.status).toBe(201); - const body = await res.json(); - expect(body.ok).toBe(true); - expect(body.body.projectID).toBe(2); - expect(body.body.amount).toBe(2000); - expect(body.body.enteredBy).toBeNull(); - expect(body.body.category).toBeNull(); + expect(body.message).toBe("Authentication required"); }); -test("post expenditure missing projectID", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - amount: 1000 - }) - }); - expect(res.status).toBe(400); - const body = await res.json(); - expect(body.message).toContain("required"); -}); - -test("post expenditure missing amount", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 1 - }) - }); - expect(res.status).toBe(400); - const body = await res.json(); - expect(body.message).toContain("required"); -}); - -test("post expenditure negative amount", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 1, - amount: -500 - }) - }); - expect(res.status).toBe(400); - const body = await res.json(); - expect(body.message).toContain("non-negative"); -}); - -test("post expenditure project not found", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 999, - amount: 1000 - }) - }); - expect(res.status).toBe(404); - const body = await res.json(); - expect(body.message).toBe("Project not found"); -}); - -test("post expenditure with invalid spentOn date", async () => { +test("401: POST with invalid token is rejected", async () => { const res = await fetch("http://localhost:3000/expenditures", { method: "POST", + headers: { + Authorization: "Bearer invalid-token", + }, body: JSON.stringify({ projectID: 1, amount: 1000, - spentOn: "not-a-date" - }) - }); - expect(res.status).toBe(400); - const body = await res.json(); - expect(body.message).toContain("valid ISO date"); -}); - -test("post expenditure user not found", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 1, - enteredBy: 999, - amount: 1000 }) }); - expect(res.status).toBe(404); + expect(res.status).toBe(401); const body = await res.json(); - expect(body.message).toBe("User not found"); + expect(body.message).toBe("Authentication required"); }); diff --git a/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts b/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts index a2dbd6f..d269c73 100644 --- a/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts +++ b/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts @@ -2,11 +2,14 @@ import { describe, test, expect, beforeEach, jest } from '@jest/globals'; // Mock the database module BEFORE importing handler jest.mock('../db'); +jest.mock('../auth'); import { handler } from '../handler'; import db from '../db'; +import { authenticateRequest } from '../auth'; const mockDb = db as any; +const mockAuthenticateRequest = authenticateRequest as jest.MockedFunction; // Helper function to create a POST event function postEvent(body: Record) { @@ -17,13 +20,114 @@ function postEvent(body: Record) { method: 'POST', }, }, + headers: { + Authorization: 'Bearer fake-token', + }, body: JSON.stringify(body), }; } +// Default authenticated admin user +const adminAuthContext = { + isAuthenticated: true as const, + user: { + cognitoSub: 'test-sub', + userId: 1, + email: 'admin@example.com', + isAdmin: true, + }, +}; + describe('POST /expenditures unit tests', () => { beforeEach(() => { jest.clearAllMocks(); + // Default: requests are from an authenticated admin + mockAuthenticateRequest.mockResolvedValue(adminAuthContext); + }); + + describe('Authentication & Authorization', () => { + test('401: unauthenticated request', async () => { + mockAuthenticateRequest.mockResolvedValue({ + isAuthenticated: false, + }); + + const res = await handler( + postEvent({ + projectID: 1, + amount: 1000, + }) + ); + + expect(res.statusCode).toBe(401); + const json = JSON.parse(res.body); + expect(json.message).toBe('Authentication required'); + }); + + test('403: user without required project role', async () => { + mockAuthenticateRequest.mockResolvedValue({ + isAuthenticated: true, + user: { + cognitoSub: 'staff-sub', + userId: 2, + email: 'staff@example.com', + isAdmin: false, + }, + }); + + // Mock: no membership found for this user on this project + mockDb.selectFrom.mockReturnValue({ + where: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + executeTakeFirst: jest.fn().mockReturnValue(null as any), + }), + }), + }), + }); + + const res = await handler( + postEvent({ + projectID: 1, + amount: 1000, + }) + ); + + expect(res.statusCode).toBe(403); + const json = JSON.parse(res.body); + expect(json.message).toContain('Unable to create expenditure'); + }); + + test('403: user with Staff role is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue({ + isAuthenticated: true, + user: { + cognitoSub: 'staff-sub', + userId: 2, + email: 'staff@example.com', + isAdmin: false, + }, + }); + + // Mock: user has Staff role + mockDb.selectFrom.mockReturnValue({ + where: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + executeTakeFirst: jest.fn().mockReturnValue({ role: 'Staff' } as any), + }), + }), + }), + }); + + const res = await handler( + postEvent({ + projectID: 1, + amount: 1000, + }) + ); + + expect(res.statusCode).toBe(403); + }); }); describe('Input Validation', () => { @@ -203,6 +307,7 @@ describe('POST /expenditures unit tests', () => { expect(json).toHaveProperty('body'); expect(json.body).toHaveProperty('projectID'); expect(json.body).toHaveProperty('amount'); + expect(json.body.enteredBy).toBe(1); // authenticated user's ID }); test('404: returns 404 when project not found', async () => { @@ -316,40 +421,5 @@ describe('POST /expenditures unit tests', () => { const json = JSON.parse(res.body); expect(json.message).toBe('Internal Server Error'); }); - - test('404: returns 404 when enteredBy user not found', async () => { - // Mock: project exists - mockDb.selectFrom.mockReturnValueOnce({ - where: jest.fn().mockReturnValue({ - selectAll: jest.fn().mockReturnValue({ - executeTakeFirst: jest.fn().mockReturnValue({ - project_id: 1, - name: 'Test Project', - } as any), - }), - }), - }); - - // Mock: user doesn't exist - mockDb.selectFrom.mockReturnValueOnce({ - where: jest.fn().mockReturnValue({ - selectAll: jest.fn().mockReturnValue({ - executeTakeFirst: jest.fn().mockReturnValue(null as any), - }), - }), - }); - - const res = await handler( - postEvent({ - projectID: 1, - amount: 1000, - enteredBy: 999, - }) - ); - - expect(res.statusCode).toBe(404); - const json = JSON.parse(res.body); - expect(json.message).toBe('User not found'); - }); }); }); From 8498f1cb0d031332f7cd1ab78f4b30b627b09159 Mon Sep 17 00:00:00 2001 From: tsudhakar87 Date: Thu, 19 Feb 2026 16:19:46 -0500 Subject: [PATCH 6/6] update e2e tests --- .../lambdas/expenditures/jest.config.js | 2 +- .../test/expenditures.e2e.test.ts | 231 +++++++++++++++--- 2 files changed, 195 insertions(+), 38 deletions(-) diff --git a/apps/backend/lambdas/expenditures/jest.config.js b/apps/backend/lambdas/expenditures/jest.config.js index 39b039e..3ed2e70 100644 --- a/apps/backend/lambdas/expenditures/jest.config.js +++ b/apps/backend/lambdas/expenditures/jest.config.js @@ -1,7 +1,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testMatch: ['**/*.unit.test.ts'], + testMatch: ['**/*.test.ts'], extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', diff --git a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts index a852395..f59b5e1 100644 --- a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts +++ b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts @@ -1,6 +1,16 @@ -import { test, expect, afterAll } from '@jest/globals'; +import { describe, test, expect, beforeEach, afterAll, jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; import { Pool } from 'pg'; +// mock auth only for now +jest.mock('../auth'); + +import { handler } from '../handler'; +import { authenticateRequest } from '../auth'; + +const mockAuthenticateRequest = authenticateRequest as jest.MockedFunction; + const pool = new Pool({ host: 'localhost', port: Number(5432), @@ -10,45 +20,192 @@ const pool = new Pool({ ssl: false, }); -afterAll(async () => { - await pool.end(); -}); +const seedSqlPath = path.resolve(__dirname, '../../../db/db_setup.sql'); +const seedSql = fs.readFileSync(seedSqlPath, 'utf8'); -test("health check", async () => { - const res = await fetch("http://localhost:3000/expenditures/health"); - expect(res.status).toBe(200); - const body = await res.json(); - expect(body.ok).toBe(true); -}); +// Helper to create Lambda events +function postEvent(body: Record) { + return { + rawPath: '/expenditures', + requestContext: { http: { method: 'POST' } }, + headers: { Authorization: 'Bearer fake-token' }, + body: JSON.stringify(body), + }; +} + +function getEvent(path: string) { + return { + rawPath: path, + requestContext: { http: { method: 'GET' } }, + headers: {}, + }; +} + +// Auth contexts based on seed data +const adminUser = { + isAuthenticated: true as const, + user: { + cognitoSub: 'admin-sub', + userId: 1, + email: 'ashley@branch.org', + isAdmin: true, + }, +}; + +// Non-admin user 1: has PI role on project 1 +const piUser = { + isAuthenticated: true as const, + user: { + cognitoSub: 'pi-sub', + userId: 1, + email: 'ashley@branch.org', + isAdmin: false, + }, +}; + +// Non-admin user 2: has Accountant role on project 1 +const accountantUser = { + isAuthenticated: true as const, + user: { + cognitoSub: 'accountant-sub', + userId: 2, + email: 'renee@branch.org', + isAdmin: false, + }, +}; -test("401: unauthenticated POST is rejected", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - body: JSON.stringify({ - projectID: 1, - amount: 1500.50, - category: "Travel", - description: "Conference flight and hotel", - spentOn: "2025-08-15" - }) +// Non-admin user 3: has Staff role on project 2, no role on project 1 +const staffUser = { + isAuthenticated: true as const, + user: { + cognitoSub: 'staff-sub', + userId: 3, + email: 'nour@branch.org', + isAdmin: false, + }, +}; + +describe('Expenditures integration tests', () => { + beforeEach(async () => { + jest.clearAllMocks(); + mockAuthenticateRequest.mockResolvedValue(adminUser); + + const client = await pool.connect(); + try { + await client.query(seedSql); + } finally { + client.release(); + } }); - expect(res.status).toBe(401); - const body = await res.json(); - expect(body.message).toBe("Authentication required"); -}); -test("401: POST with invalid token is rejected", async () => { - const res = await fetch("http://localhost:3000/expenditures", { - method: "POST", - headers: { - Authorization: "Bearer invalid-token", - }, - body: JSON.stringify({ - projectID: 1, - amount: 1000, - }) + afterAll(async () => { + await pool.end(); + }); + + describe('Health check', () => { + test('200: health check returns ok', async () => { + const res = await handler(getEvent('/expenditures/health')); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(body.ok).toBe(true); + expect(body.timestamp).toBeDefined(); + }); + }); + + describe('Authentication', () => { + test('401: unauthenticated request is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue({ isAuthenticated: false }); + + const res = await handler(postEvent({ projectID: 1, amount: 1000 })); + expect(res.statusCode).toBe(401); + expect(JSON.parse(res.body).message).toBe('Authentication required'); + }); + }); + + describe('Authorization', () => { + test('201: PI can create expenditure on their project', async () => { + mockAuthenticateRequest.mockResolvedValue(piUser); + + const res = await handler(postEvent({ projectID: 1, amount: 500 })); + expect(res.statusCode).toBe(201); + }); + + test('201: Accountant can create expenditure on their project', async () => { + mockAuthenticateRequest.mockResolvedValue(accountantUser); + + // User 2 is Accountant on project 1 + const res = await handler(postEvent({ projectID: 1, amount: 750 })); + expect(res.statusCode).toBe(201); + expect(JSON.parse(res.body).body.enteredBy).toBe(2); + }); + + test('403: Staff cannot create expenditure on their project', async () => { + mockAuthenticateRequest.mockResolvedValue(staffUser); + + const res = await handler(postEvent({ projectID: 2, amount: 500 })); + expect(res.statusCode).toBe(403); + }); + + test('403: user with no membership on project is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue(staffUser); + + // User 3 has no membership on project 1 + const res = await handler(postEvent({ projectID: 1, amount: 500 })); + expect(res.statusCode).toBe(403); + }); + }); + + describe('Success cases', () => { + test('201: admin creates expenditure with all fields', async () => { + const res = await handler( + postEvent({ + projectID: 1, + amount: 1500.50, + category: 'Travel', + description: 'Conference flight and hotel', + spentOn: '2025-08-15', + }) + ); + + expect(res.statusCode).toBe(201); + const body = JSON.parse(res.body); + expect(body.ok).toBe(true); + expect(body.body.projectID).toBe(1); + expect(body.body.enteredBy).toBe(1); + expect(body.body.amount).toBe(1500.50); + expect(body.body.category).toBe('Travel'); + expect(body.body.description).toBe('Conference flight and hotel'); + expect(body.body.spentOn).toBe('2025-08-15'); + + // Verify the row was actually written to the DB + const client = await pool.connect(); + try { + const result = await client.query( + "SELECT * FROM branch.expenditures WHERE category = 'Travel' AND amount = 1500.50" + ); + expect(result.rows.length).toBe(1); + expect(result.rows[0].entered_by).toBe(1); + expect(result.rows[0].project_id).toBe(1); + } finally { + client.release(); + } + }); + + test('201: admin creates expenditure with required fields only', async () => { + const res = await handler(postEvent({ projectID: 2, amount: 2000 })); + + expect(res.statusCode).toBe(201); + const body = JSON.parse(res.body); + expect(body.ok).toBe(true); + expect(body.body.projectID).toBe(2); + expect(body.body.amount).toBe(2000); + expect(body.body.category).toBeNull(); + }); + + test('404: project not found', async () => { + const res = await handler(postEvent({ projectID: 999, amount: 1000 })); + expect(res.statusCode).toBe(404); + expect(JSON.parse(res.body).message).toBe('Project not found'); + }); }); - expect(res.status).toBe(401); - const body = await res.json(); - expect(body.message).toBe("Authentication required"); });