From 02592a14dd771e698979798e182c929646eaabee Mon Sep 17 00:00:00 2001 From: miguel-merlin Date: Thu, 12 Dec 2024 01:58:22 -0500 Subject: [PATCH] feat: enabled auth with context --- package-lock.json | 299 ++++++++++++++++++++++++++- package.json | 5 + src/api/apiClient.ts | 15 ++ src/api/lib/organizations.ts | 35 ++++ src/auth/AuthContext.ts | 23 +++ src/auth/AuthProvider.tsx | 240 +++++++++++++++++++++ src/hooks/useAuthenticatedClient.tsx | 65 ++++++ src/interface/Request.ts | 7 + src/main.tsx | 5 +- src/pages/Home.tsx | 19 ++ 10 files changed, 704 insertions(+), 9 deletions(-) create mode 100644 src/api/apiClient.ts create mode 100644 src/api/lib/organizations.ts create mode 100644 src/auth/AuthContext.ts create mode 100644 src/auth/AuthProvider.tsx create mode 100644 src/hooks/useAuthenticatedClient.tsx create mode 100644 src/interface/Request.ts create mode 100644 src/pages/Home.tsx diff --git a/package-lock.json b/package-lock.json index cd91c31..71d1fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@types/react-dom": "^19.0.2", + "amazon-cognito-identity-js": "^6.3.12", + "axios": "^1.7.9", + "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -16,6 +19,8 @@ "@eslint/js": "^9.15.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@types/node": "^22.10.2", + "@types/prop-types": "^15.7.14", "@types/react": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", @@ -70,6 +75,62 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@aws-sdk/types": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.709.0.tgz", + "integrity": "sha512-ArtLTMxgjf13Kfu3gWH3Ez9Q5TkDdcRZUofpKH3pMGB/C6KAbeSCtIIDKfoRTUABzyGlPyCrZdnFjKyH+ypIpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1385,6 +1446,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -1550,6 +1623,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", @@ -1943,6 +2033,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amazon-cognito-identity-js": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.12.tgz", + "integrity": "sha512-s7NKDZgx336cp+oDeUtB2ZzT8jWJp/v2LWuYl+LQtMEODe22RF1IJ4nRiDATp+rp1pTffCZcm44Quw4jx2bqNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -2151,7 +2254,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -2208,6 +2310,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2215,6 +2328,26 @@ "dev": true, "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/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2285,6 +2418,23 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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/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/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2492,7 +2642,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -2729,7 +2878,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3371,6 +3519,12 @@ "node": ">=12.0.0" } }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3503,6 +3657,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3534,7 +3708,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3921,6 +4094,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4404,6 +4597,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -4430,6 +4633,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4699,7 +4908,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4709,7 +4917,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -4806,6 +5013,48 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4844,7 +5093,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5385,6 +5633,29 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6393,7 +6664,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -6540,6 +6810,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/package.json b/package.json index 697b62c..db656d6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ }, "dependencies": { "@types/react-dom": "^19.0.2", + "amazon-cognito-identity-js": "^6.3.12", + "axios": "^1.7.9", + "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -21,6 +24,8 @@ "@eslint/js": "^9.15.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", + "@types/node": "^22.10.2", + "@types/prop-types": "^15.7.14", "@types/react": "^19.0.1", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts new file mode 100644 index 0000000..f4d416e --- /dev/null +++ b/src/api/apiClient.ts @@ -0,0 +1,15 @@ +import axios, { AxiosRequestConfig } from 'axios'; + +const getApiClient = (baseUrl: string) => { + const apiClient = axios.create({ + baseURL: baseUrl, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + withCredentials: true, + } satisfies AxiosRequestConfig); + return apiClient; +}; + +export default getApiClient; diff --git a/src/api/lib/organizations.ts b/src/api/lib/organizations.ts new file mode 100644 index 0000000..06dc43a --- /dev/null +++ b/src/api/lib/organizations.ts @@ -0,0 +1,35 @@ +import { AxiosResponse, AxiosError } from 'axios'; +import { RequestParamsI } from '../../interface/Request'; + +interface OrganizationError extends Error { + statusCode?: number; +} + +/** + * Fetches all organizations from the API + * @param request - Authenticated request function + * @returns Promise resolving to the API response + * @throws {OrganizationError} When the API request fails + */ +export const getAllOrganizations = async ( + request: ({ method, uri, body }: RequestParamsI) => Promise +): Promise => { + try { + const response = await request({ + method: 'GET', + uri: '/api/organizations', + body: {}, + }); + return response; + } catch (error) { + const organizationError: OrganizationError = new Error( + (error as Error).message || 'Failed to fetch organizations' + ); + + if ((error as AxiosError).isAxiosError) { + const axiosError = error as AxiosError; + organizationError.statusCode = axiosError.response?.status; + } + throw organizationError; + } +}; diff --git a/src/auth/AuthContext.ts b/src/auth/AuthContext.ts new file mode 100644 index 0000000..4316b34 --- /dev/null +++ b/src/auth/AuthContext.ts @@ -0,0 +1,23 @@ +import { createContext } from 'react'; +import { AuthContextType } from './AuthProvider'; +import { CognitoUser } from 'amazon-cognito-identity-js'; + +export interface State { + isAuthenticated: boolean; + isInitialized: boolean; + user: CognitoUser | null; +} + +export const initialState: State = { + isAuthenticated: false, + isInitialized: false, + user: null, +}; + +export const AuthContext = createContext({ + ...initialState, + method: 'cognito', + login: () => Promise.resolve(), + logout: () => Promise.resolve(), + register: () => {}, +}); diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx new file mode 100644 index 0000000..e8e8433 --- /dev/null +++ b/src/auth/AuthProvider.tsx @@ -0,0 +1,240 @@ +import { + AuthenticationDetails, + CognitoUser, + CognitoUserPool, + CognitoUserAttribute, + CognitoUserSession, +} from 'amazon-cognito-identity-js'; +import { AuthContext, State, initialState } from './AuthContext'; +import { useCallback, useEffect, useReducer, ReactNode } from 'react'; +import axios from 'axios'; + +export const UserPool = new CognitoUserPool({ + UserPoolId: process.env.REACT_APP_USER_POOL_ID || '', + ClientId: process.env.REACT_APP_CLIENT_ID || '', +}); + +interface Action { + type: string; + payload?: { + isAuthenticated?: boolean; + user?: CognitoUser | null; + }; +} + +interface UserAttributes { + [key: string]: string; +} + +interface Session { + getIdToken: () => { + getJwtToken: () => string; + }; +} + +interface GetSessionResult { + user: CognitoUser; + session: Session; + headers: { Authorization: string }; + attributes: UserAttributes; +} + +export interface AuthContextType extends State { + method: 'cognito'; + login: ( + email: string, + password: string + ) => Promise; + logout: () => void; + register: ( + email: string, + password: string, + firstName: string, + lastName: string + ) => void; +} + +const handlers: { [key: string]: (state: State, action: Action) => State } = { + AUTHENTICATE: (state: State, action: Action) => { + const { isAuthenticated, user } = action.payload!; + return { + ...state, + isAuthenticated: isAuthenticated!, + isInitialized: true, + user: user ?? null, + }; + }, + LOGOUT: (state: State) => ({ + ...state, + isAuthenticated: false, + user: null, + }), +}; + +const reducer = (state: State, action: Action): State => + handlers[action.type] ? handlers[action.type](state, action) : state; + +interface AuthProviderProps { + children: ReactNode; +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [state, dispatch] = useReducer(reducer, initialState); + + const getUserAttributes = useCallback( + (currentUser: CognitoUser): Promise => + new Promise((resolve, reject) => { + currentUser.getUserAttributes((err, attributes) => { + if (err) { + reject(err); + return; + } + const results: UserAttributes = {}; + attributes?.forEach((attribute) => { + results[attribute.Name] = attribute.Value; + }); + resolve(results); + }); + }), + [] + ); + + const getSession = useCallback( + () => + new Promise((resolve, reject) => { + const user = UserPool.getCurrentUser(); + if (user) { + user.getSession(async (error: Error | null, session: Session) => { + if (error) { + reject(error); + return; + } + const attributes: UserAttributes = await getUserAttributes(user); + const token: string = session.getIdToken().getJwtToken(); + axios.defaults.headers.common.Authorization = token; + dispatch({ + type: 'AUTHENTICATE', + payload: { + isAuthenticated: true, + user, + }, + }); + resolve({ + user, + session, + headers: { Authorization: token }, + attributes, + } as GetSessionResult); + }); + } else { + dispatch({ + type: 'AUTHENTICATE', + payload: { + isAuthenticated: false, + user: null, + }, + }); + } + }), + [getUserAttributes] + ); + + const initial = useCallback(async () => { + try { + await getSession(); + } catch { + dispatch({ + type: 'AUTHENTICATE', + payload: { + isAuthenticated: false, + user: null, + }, + }); + } + }, [getSession]); + + useEffect(() => { + initial(); + }, [initial]); + + const login = useCallback( + ( + email: string, + password: string + ): Promise => + new Promise((resolve, reject) => { + const user = new CognitoUser({ + Username: email, + Pool: UserPool, + }); + + const authDetails = new AuthenticationDetails({ + Username: email, + Password: password, + }); + + user.authenticateUser(authDetails, { + onSuccess: (data) => { + getSession(); + resolve(data as CognitoUserSession); + }, + onFailure: (err) => { + reject(err); + return; + }, + newPasswordRequired: () => { + resolve({ message: 'newPasswordRequired' }); + }, + }); + }), + [getSession] + ); + + const logout = () => { + const user = UserPool.getCurrentUser(); + if (user) { + user.signOut(); + dispatch({ type: 'LOGOUT' }); + } + }; + + const register = ( + email: string, + password: string, + firstName: string, + lastName: string + ) => { + UserPool.signUp( + email, + password, + [ + new CognitoUserAttribute({ Name: 'email', Value: email }), + new CognitoUserAttribute({ + Name: 'name', + Value: `${firstName} ${lastName}`, + }), + ], + [], + (err) => { + if (err) { + throw err; + } + window.location.href = '/login'; + } + ); + }; + + return ( + + {children} + + ); +} diff --git a/src/hooks/useAuthenticatedClient.tsx b/src/hooks/useAuthenticatedClient.tsx new file mode 100644 index 0000000..62dd238 --- /dev/null +++ b/src/hooks/useAuthenticatedClient.tsx @@ -0,0 +1,65 @@ +import { useContext } from 'react'; +import { AuthContext } from '../auth/AuthContext'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import getApiClient from '../api/apiClient'; +import { RequestParamsI } from '../interface/Request'; + +const apiClient = getApiClient('https://localhost:8080'); + +export const useAuthenticatedClient = (): (( + params: RequestParamsI +) => Promise) => { + const { user, isAuthenticated } = useContext(AuthContext); + const request = async ({ + method, + uri, + body, + }: RequestParamsI): Promise => { + if (!isAuthenticated || !user) { + throw new Error('User is not authenticated'); + } + + return new Promise((resolve, reject) => { + user.getSession( + ( + err: Error | null, + session: { getIdToken: () => { getJwtToken: () => string } } + ) => { + if (err) { + reject(err); + return; + } + + const token = session.getIdToken().getJwtToken(); + const config: AxiosRequestConfig = { + method, + url: uri, + headers: { + Authorization: `Bearer ${token}`, + }, + data: body, + }; + apiClient + .request(config) + .then(resolve) + .catch((error) => { + switch (error.response.status) { + case 400: + reject(new Error('Bad Request')); + break; + case 401: + reject(new Error('Unauthorized')); + break; + case 403: + reject(new Error('Forbidden')); + break; + default: + reject(error); + } + }); + } + ); + }); + }; + return request; +}; diff --git a/src/interface/Request.ts b/src/interface/Request.ts new file mode 100644 index 0000000..37a6aee --- /dev/null +++ b/src/interface/Request.ts @@ -0,0 +1,7 @@ +import { Method } from 'axios'; + +export interface RequestParamsI { + method: Method; + uri: string; + body?: Record; +} diff --git a/src/main.tsx b/src/main.tsx index ea9e363..de4634c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { AuthProvider } from './auth/AuthProvider.tsx'; import App from './App.tsx'; import './index.css'; createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..02be32c --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { useAuthenticatedClient } from '../hooks/useAuthenticatedClient'; +import { getAllOrganizations } from '../api/lib/organizations'; + +export const Home = () => { + const client = useAuthenticatedClient(); + + useEffect(() => { + const fetchOrganizations = async () => { + try { + const response = await getAllOrganizations(client); + console.log(response.data); + } catch (error) { + console.error(error); + } + }; + fetchOrganizations(); + }, [client]); +};