diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml
index bf364cb9..0a79ef2c 100644
--- a/.github/workflows/analysis.yml
+++ b/.github/workflows/analysis.yml
@@ -55,6 +55,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: bcgov-nr/action-test-and-analyse@v1.2.1
+ env:
+ VITE_ZONE: test
+ VITE_USER_POOLS_ID: ca-central-1_abc123
+ VITE_USER_POOLS_WEB_CLIENT_ID: abc123
+ VITE_BACKEND_URL: http://localhost:8080
with:
commands: |
npm ci
diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml
index 8d4a8bd4..b0f39c68 100644
--- a/.github/workflows/merge.yml
+++ b/.github/workflows/merge.yml
@@ -34,6 +34,7 @@ jobs:
-p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}'
-p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}'
-p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }}
+ -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }}
-p ZONE=test
deploys-test:
@@ -106,6 +107,7 @@ jobs:
-p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}'
-p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}'
-p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }}
+ -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }}
-p ZONE=prod
image-promotions:
diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml
index 6a009bf0..b7ccb11d 100644
--- a/.github/workflows/pr-open.yml
+++ b/.github/workflows/pr-open.yml
@@ -65,6 +65,7 @@ jobs:
-p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}'
-p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}'
-p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }}
+ -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }}
triggers: ('common/' 'backend/' 'frontend/')
builds:
diff --git a/common/openshift.init.yml b/common/openshift.init.yml
index f6f83b4b..1249a65e 100644
--- a/common/openshift.init.yml
+++ b/common/openshift.init.yml
@@ -40,6 +40,9 @@ parameters:
- name: VITE_USER_POOLS_WEB_CLIENT_ID
description: Cognito user pools web client ID
required: true
+ - name: VITE_USER_POOLS_ID
+ description: Cognito user pools ID
+ required: true
objects:
- apiVersion: v1
kind: Secret
@@ -95,6 +98,7 @@ objects:
app: ${NAME}-${ZONE}
stringData:
vite-user-pools-web-client-id: ${VITE_USER_POOLS_WEB_CLIENT_ID}
+ vite-user-pools-id: ${VITE_USER_POOLS_ID}
- apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
diff --git a/frontend/Caddyfile b/frontend/Caddyfile
index a3a8ed2c..21770316 100644
--- a/frontend/Caddyfile
+++ b/frontend/Caddyfile
@@ -28,7 +28,7 @@
}
handle /env.js {
header Content-Type "text/javascript"
- respond `window.config = {"VITE_USER_POOLS_WEB_CLIENT_ID":"{$VITE_USER_POOLS_WEB_CLIENT_ID}","VITE_ZONE":"{$VITE_ZONE}","VITE_BACKEND_URL":"{$VITE_BACKEND_URL}"};`
+ respond `window.config = {"VITE_USER_POOLS_ID":"{$VITE_USER_POOLS_ID}","VITE_USER_POOLS_WEB_CLIENT_ID":"{$VITE_USER_POOLS_WEB_CLIENT_ID}","VITE_ZONE":"{$VITE_ZONE}","VITE_BACKEND_URL":"{$VITE_BACKEND_URL}"};`
}
handle_path /* {
diff --git a/frontend/openshift.deploy.yml b/frontend/openshift.deploy.yml
index f20f6f6c..34ed6e50 100644
--- a/frontend/openshift.deploy.yml
+++ b/frontend/openshift.deploy.yml
@@ -91,6 +91,11 @@ objects:
secretKeyRef:
name: ${NAME}-${ZONE}-frontend
key: vite-user-pools-web-client-id
+ - name: VITE_USER_POOLS_ID
+ valueFrom:
+ secretKeyRef:
+ name: ${NAME}-${ZONE}-frontend
+ key: vite-user-pools-id
ports:
- containerPort: 3000
protocol: TCP
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7191f4e7..597bbb91 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -18,8 +18,7 @@
"@types/node": "^22.0.0",
"@vitejs/plugin-react": "^4.0.4",
"@vitejs/plugin-react-swc": "^3.3.2",
- "amazon-cognito-identity-js": "^6.3.13",
- "aws-amplify": "^6.5.0",
+ "aws-amplify": "^6.7.0",
"axios": "^1.6.8",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.8.3",
@@ -134,31 +133,6 @@
"uuid": "^9.0.0"
}
},
- "node_modules/@aws-amplify/api-graphql/node_modules/@aws-sdk/types": {
- "version": "3.387.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.387.0.tgz",
- "integrity": "sha512-YTjFabNwjTF+6yl88f0/tWff018qmmgMmjlw45s6sdVKueWxdxV68U7gepNLF2nhaQPZa6FDOBoA51NaviVs0Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^2.1.0",
- "tslib": "^2.5.0"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-amplify/api-graphql/node_modules/@smithy/types": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz",
- "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-amplify/api-rest": {
"version": "4.0.56",
"resolved": "https://registry.npmjs.org/@aws-amplify/api-rest/-/api-rest-4.0.56.tgz",
@@ -199,31 +173,6 @@
"uuid": "^9.0.0"
}
},
- "node_modules/@aws-amplify/core/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-amplify/core/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
"node_modules/@aws-amplify/core/node_modules/@aws-sdk/types": {
"version": "3.398.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.398.0.tgz",
@@ -249,15 +198,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/@aws-amplify/core/node_modules/js-cookie": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
- "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@aws-amplify/data-schema": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.12.1.tgz",
@@ -366,17 +306,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-crypto/crc32/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
"node_modules/@aws-crypto/sha256-browser": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
@@ -392,7 +321,7 @@
"tslib": "^2.6.2"
}
},
- "node_modules/@aws-crypto/sha256-browser/node_modules/@aws-crypto/sha256-js": {
+ "node_modules/@aws-crypto/sha256-js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
"integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
@@ -406,34 +335,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-crypto/sha256-browser/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "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/supports-web-crypto": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
@@ -444,22 +345,16 @@
}
},
"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==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.1.0",
- "@aws-sdk/util-utf8-browser": "^3.0.0",
- "tslib": "^1.11.1"
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
}
},
- "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/client-firehose": {
"version": "3.621.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.621.0.tgz",
@@ -512,57 +407,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -576,18 +420,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-firehose/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-firehose/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -657,57 +489,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-kinesis/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -721,18 +502,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-kinesis/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-kinesis/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -798,57 +567,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -862,18 +580,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-personalize-events/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-personalize-events/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -989,57 +695,6 @@
"@aws-sdk/client-sts": "^3.621.0"
}
},
- "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -1053,18 +708,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -1078,57 +721,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -1142,18 +734,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -1218,57 +798,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/sha256-js": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/util": "^5.2.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@smithy/util-utf8": "^2.0.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": {
"version": "3.609.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz",
@@ -1282,18 +811,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz",
@@ -1750,16 +1267,28 @@
}
},
"node_modules/@aws-sdk/types": {
- "version": "3.679.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz",
- "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==",
+ "version": "3.387.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.387.0.tgz",
+ "integrity": "sha512-YTjFabNwjTF+6yl88f0/tWff018qmmgMmjlw45s6sdVKueWxdxV68U7gepNLF2nhaQPZa6FDOBoA51NaviVs0Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^2.1.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types/node_modules/@smithy/types": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz",
+ "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^3.5.0",
"tslib": "^2.6.2"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/util-endpoints": {
@@ -1863,15 +1392,6 @@
"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",
@@ -6285,19 +5805,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/amazon-cognito-identity-js": {
- "version": "6.3.14",
- "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.14.tgz",
- "integrity": "sha512-nxN8L5AAwLIsgQKyKMOsNwr5xeY7+fccv+A/ALiYxmGiM341XX0dcoMuM+LlJmzfIfuPmTrXSehhTunTTQFAow==",
- "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": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -9005,12 +8512,6 @@
"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",
@@ -10258,16 +9759,6 @@
"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/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -10521,10 +10012,13 @@
}
},
"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"
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
},
"node_modules/js-tokens": {
"version": "4.0.0",
@@ -11123,48 +10617,6 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"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.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@@ -13252,12 +12704,6 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"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/frontend/package.json b/frontend/package.json
index 8e667992..355b73bc 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,9 +13,8 @@
"@tanstack/react-query": "^5.50.1",
"@types/node": "^22.0.0",
"@vitejs/plugin-react": "^4.0.4",
- "@vitejs/plugin-react-swc": "^3.3.2",
- "amazon-cognito-identity-js": "^6.3.13",
- "aws-amplify": "^6.5.0",
+ "@vitejs/plugin-react-swc": "^3.3.2",
+ "aws-amplify": "^6.7.0",
"axios": "^1.6.8",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.8.3",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2d44cba2..79bc4953 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,52 +1,59 @@
-import {
- BrowserRouter, Routes, Route
-} from 'react-router-dom';
-import { Amplify } from 'aws-amplify';
-import amplifyconfig from './amplifyconfiguration';
-
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import './custom.scss';
-
import Landing from "./screens/Landing";
import Help from "./screens/Help";
import Reports from './screens/Reports';
import SideLayout from './layouts/SideLayout';
-import PostLoginRoute from './routes/PostLoginRoute';
import ProtectedRoute from './routes/ProtectedRoute';
import Opening from './screens/Opening';
import DashboardRedirect from './screens/DashboardRedirect';
import SilvicultureSearch from './screens/SilvicultureSearch';
+import ErrorHandling from './screens/ErrorHandling';
+
-Amplify.configure(amplifyconfig);
+// Create the router instance
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: // Handle errors for the Landing page route
+ },
+ {
+ path: "/dashboard",
+ element: ,
+ errorElement: // Handle errors for the dashboard route
+ },
+ {
+ element: ,
+ errorElement: , // Global error element for protected routes
+ children: [
+ {
+ path: "/opening",
+ element: } />
+ },
+ {
+ path: "/silviculture-search",
+ element: } />
+ },
+ {
+ path: "/opening/reports",
+ element: } />
+ },
+ {
+ path: "/help",
+ element: } />
+ }
+ ]
+ },
+ // Catch-all route for unmatched paths
+ {
+ path: "*",
+ element:
+ }
+]);
const App: React.FC = () => {
- return (
-
-
- } />
-
-
-
- } />
-
- } />
-
- } />
-
- } />
-
- } />
-
- } />
-
- } />
- } />} />
-
-
- );
+ return ;
};
export default App;
diff --git a/frontend/src/__test__/actions/userAction.test.ts b/frontend/src/__test__/actions/userAction.test.ts
new file mode 100644
index 00000000..592f409f
--- /dev/null
+++ b/frontend/src/__test__/actions/userAction.test.ts
@@ -0,0 +1,86 @@
+import { describe, it, expect, vi } from 'vitest';
+import { getUserDetails, setClientRoles } from '../../actions/userAction';
+import {
+ USER_DETAILS_REQUEST,
+USER_DETAILS_SUCCESS,
+USER_DETAILS_FAIL,
+SET_CLIENT_ROLES
+} from '../../constants/userConstants';
+import { useGetAuth } from '../../contexts/AuthProvider';
+import { AppDispatch } from '../../store';
+import { UserClientRolesType } from '../../types/UserRoleType';
+
+
+
+vi.mock('../../contexts/AuthProvider', () => ({
+useGetAuth: vi.fn(),
+}));
+
+describe('userAction', () => {
+describe('getUserDetails', () => {
+ it('should dispatch USER_DETAILS_REQUEST and USER_DETAILS_SUCCESS with user data when successful', async () => {
+ const mockDispatch = vi.fn();
+ const mockIsLoggedIn = true;
+ const mockUser = { firstName: 'John', lastName: 'Doe' };
+ const mockUserJSON = JSON.stringify(mockUser);
+
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn });
+ localStorage.setItem('famLoginUser', mockUserJSON);
+
+ await getUserDetails()(mockDispatch as unknown as AppDispatch);
+
+ expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST });
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: USER_DETAILS_SUCCESS,
+ payload: { ...mockUser, isLoggedIn: mockIsLoggedIn },
+ });
+ });
+
+ it('should dispatch USER_DETAILS_FAIL with error when an error occurs', async () => {
+ const mockDispatch = vi.fn();
+ const mockError = new Error('Test error');
+
+ (useGetAuth as vi.Mock).mockImplementation(() => {
+ throw mockError;
+ });
+
+ await getUserDetails()(mockDispatch as unknown as AppDispatch);
+
+ expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST });
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: USER_DETAILS_FAIL,
+ payload: { error: mockError },
+ });
+ });
+
+ it('should handle missing user data in localStorage', async () => {
+ const mockDispatch = vi.fn();
+ const mockIsLoggedIn = true;
+
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn });
+ localStorage.removeItem('famLoginUser');
+
+ await getUserDetails()(mockDispatch as unknown as AppDispatch);
+
+ expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST });
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: USER_DETAILS_SUCCESS,
+ payload: { isLoggedIn: mockIsLoggedIn },
+ });
+ });
+});
+
+describe('setClientRoles', () => {
+ it('should dispatch SET_CLIENT_ROLES with client roles', () => {
+ const mockDispatch = vi.fn();
+ const mockClientRoles: UserClientRolesType[] = [{ clientId: '123', roles: ['admin'] }];
+
+ setClientRoles(mockClientRoles)(mockDispatch as unknown as AppDispatch);
+
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: SET_CLIENT_ROLES,
+ payload: mockClientRoles,
+ });
+ });
+});
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/BCHeaderwSide.test.tsx b/frontend/src/__test__/components/BCHeaderwSide.test.tsx
index f3aa1fea..c3a49c78 100644
--- a/frontend/src/__test__/components/BCHeaderwSide.test.tsx
+++ b/frontend/src/__test__/components/BCHeaderwSide.test.tsx
@@ -10,6 +10,7 @@ import { Provider } from 'react-redux';
import store from '../../store';
import { UserClientRolesType } from '../../types/UserRoleType';
import '@testing-library/jest-dom';
+import { AuthProvider } from '../../contexts/AuthProvider';
vi.mock('../../services/TestService', () => ({
getForestClientByNumberOrAcronym: vi.fn(() => [
@@ -29,13 +30,15 @@ const renderComponent = () => {
const qc = new QueryClient();
render(
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/frontend/src/__test__/components/MyProfile.test.tsx b/frontend/src/__test__/components/MyProfile.test.tsx
new file mode 100644
index 00000000..5e723ce3
--- /dev/null
+++ b/frontend/src/__test__/components/MyProfile.test.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import React from 'react';
+import { render, act, waitFor, fireEvent, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import MyProfile from '../../components/MyProfile';
+import { useThemePreference } from '../../utils/ThemePreference';
+import { useGetAuth } from '../../contexts/AuthProvider';
+
+// Mock dependencies
+vi.mock('../../utils/ThemePreference', () => ({
+ useThemePreference: vi.fn(),
+}));
+
+vi.mock('../../contexts/AuthProvider', () => ({
+ useGetAuth: vi.fn(),
+}));
+
+describe('MyProfile Component', () => {
+ const mockSetTheme = vi.fn();
+ const mockLogout = vi.fn();
+ const mockAuthUser = {
+ firstName: 'John',
+ lastName: 'Doe',
+ userName: 'johndoe',
+ email: 'john.doe@example.com',
+ };
+
+ beforeEach(() => {
+ (useThemePreference as vi.Mock).mockReturnValue({
+ theme: 'g10',
+ setTheme: mockSetTheme,
+ });
+ (useGetAuth as vi.Mock).mockReturnValue({
+ logout: mockLogout,
+ user: mockAuthUser,
+ });
+
+ // Mock localStorage
+ vi.spyOn(Storage.prototype, 'setItem');
+ });
+
+ it('should render user information correctly', () => {
+ render();
+ expect(screen.getByText('John Doe')).toBeDefined();
+ expect(screen.getByText('IDIR: johndoe')).toBeDefined();
+ expect(screen.getByText('Email:john.doe@example.com')).toBeDefined();
+ });
+
+ it('should change theme when "Change theme" button is clicked', () => {
+ render();
+ const changeThemeButton = screen.getByText('Change theme');
+ fireEvent.click(changeThemeButton);
+ expect(mockSetTheme).toHaveBeenCalledWith('g100');
+ expect(localStorage.setItem).toHaveBeenCalledWith('mode', 'dark');
+ });
+
+ it('should call logout function when "Log out" button is clicked', () => {
+ render();
+ const logoutButton = screen.getByText('Log out');
+ fireEvent.click(logoutButton);
+ expect(mockLogout).toHaveBeenCalled();
+ });
+
+ it('should render organization selection section', () => {
+ render();
+ expect(screen.getByText('Select organization')).toBeDefined();
+ });
+
+ it('should render options section', () => {
+ render();
+ expect(screen.getByText('Options')).toBeDefined();
+ });
+
+ it('should toggle theme between light and dark', () => {
+ (useThemePreference as vi.Mock).mockReturnValueOnce({
+ theme: 'g100',
+ setTheme: mockSetTheme,
+ });
+ render();
+ const changeThemeButton = screen.getByText('Change theme');
+ fireEvent.click(changeThemeButton);
+ expect(mockSetTheme).toHaveBeenCalledWith('g10');
+ expect(localStorage.setItem).toHaveBeenCalledWith('mode', 'light');
+ });
+});
diff --git a/frontend/src/__test__/contexts/AuthProvider.test.tsx b/frontend/src/__test__/contexts/AuthProvider.test.tsx
new file mode 100644
index 00000000..3430ccca
--- /dev/null
+++ b/frontend/src/__test__/contexts/AuthProvider.test.tsx
@@ -0,0 +1,173 @@
+import React from 'react';
+import { render, act, waitFor } from '@testing-library/react';
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { AuthProvider, useGetAuth } from '../../contexts/AuthProvider';
+import { signInWithRedirect, signOut } from 'aws-amplify/auth';
+import { parseToken } from '../../services/AuthService';
+import { extractGroups } from '../../utils/famUtils';
+import { env } from '../../env';
+
+vi.mock('aws-amplify/auth', () => ({
+ signInWithRedirect: vi.fn(),
+ signOut: vi.fn(),
+}));
+
+function setAuthCookies(value: string | null) {
+ const cookieName = `CognitoIdentityServiceProvider.${env.VITE_USER_POOLS_WEB_CLIENT_ID}`;
+ document.cookie = `${cookieName}.LastAuthUser=; path=/; max-age=0`;
+ document.cookie = `${cookieName}.abci21.idToken=; path=/; max-age=0`;
+
+ if (value) {
+ document.cookie = `${cookieName}.LastAuthUser=abci21; path=/`;
+ document.cookie = `${cookieName}.abci21.idToken=${value}; path=/`;
+ }
+}
+
+const sampleAuthToken = 'eyJhbGciOiJIUzI1NiJ9.eyJjb2duaXRvOmdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYjVlY2RiMDk0ZGZiNDE0OWE2YTg0NDVhMDFhOTZiZjBAaWRpciIsImN1c3RvbTppZHBfdXNlcl9pZCI6IkI1RUNEQjA5NERGQjQxNDlBNkE4NDQ1QTAxQTk2QkYwIiwiY3VzdG9tOmlkcF91c2VybmFtZSI6IkpSWUFOIiwiY3VzdG9tOmlkcF9kaXNwbGF5X25hbWUiOiJSeWFuLCBKYWNrIENJQTpJTiIsImVtYWlsIjoiamFjay5yeWFuQGdvdi5iYy5jYSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY3VzdG9tOmlkcF9uYW1lIjoiaWRpciIsImdpdmVuX25hbWUiOiJKYWNrIiwibmFtZSI6IkphY2sgUnlhbiIsImZhbWlseV9uYW1lIjoiUnlhbiJ9.cLEC8Yh08HErgP2x33pgt2koYJlFNRfi7ja7etcabrM';
+
+describe('AuthProvider', () => {
+ const mockUser = { firstName: 'John', lastName: 'Doe' };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should initialize correctly', async () => {
+ setAuthCookies(sampleAuthToken);
+
+ const TestComponent = () => {
+ const { user, isLoggedIn, isLoading } = useGetAuth();
+ return (
+
+ {isLoading ? 'Loading' : 'Loaded'}
+ {isLoggedIn ? 'Logged In' : 'Logged Out'}
+ {user?.firstName}
+
+ );
+ };
+
+ let getByText;
+ await act(async () => {
+ ({ getByText } = render(
+
+
+
+ ));
+ });
+
+ await waitFor(() => expect(getByText('Loaded')).toBeDefined());
+ expect(getByText('Logged In')).toBeDefined();
+ expect(getByText('Jack')).toBeDefined();
+ });
+
+ it('should find no token', async () => {
+ setAuthCookies(null);
+
+ const TestComponent = () => {
+ const { user, isLoggedIn, isLoading } = useGetAuth();
+ return (
+
+ {isLoading ? 'Loading' : 'Loaded'}
+ {isLoggedIn ? 'Logged In' : 'Logged Out'}
+ Welcome {user?.firstName || 'nobody'}
+
+ );
+ };
+
+ let getByText;
+ await act(async () => {
+ ({ getByText } = render(
+
+
+
+ ));
+ });
+
+ await waitFor(() => expect(getByText('Loaded')).toBeDefined());
+ expect(getByText('Logged Out')).toBeDefined();
+ expect(getByText('Welcome nobody')).toBeDefined();
+ });
+
+ it('should handle login correctly', async () => {
+ setAuthCookies(sampleAuthToken);
+ const provider = 'idir';
+ const envProvider = 'TEST-IDIR';
+
+ const TestComponent = () => {
+ const { login } = useGetAuth();
+ return ;
+ };
+
+ let getByText;
+ await act(async () => {
+ ({ getByText } = render(
+
+
+
+ ));
+ });
+
+ act(() => {
+ getByText('Login').click();
+ });
+
+ expect(signInWithRedirect).toHaveBeenCalledWith({
+ provider: { custom: envProvider.toUpperCase() },
+ });
+ });
+
+ it('should handle logout correctly', async () => {
+ setAuthCookies(sampleAuthToken);
+
+ const TestComponent = () => {
+ const { logout } = useGetAuth();
+ return ;
+ };
+
+ let getByText;
+ await act(async () => {
+ ({ getByText } = render(
+
+
+
+ ));
+ });
+
+ act(() => {
+ getByText('Logout').click();
+ });
+
+ await waitFor(() => expect(signOut).toHaveBeenCalled());
+ });
+
+ it('should handle userDetails correctly', async () => {
+ setAuthCookies(sampleAuthToken);
+
+ const TestComponent = () => {
+ const { user } = useGetAuth();
+ return {user ? user.firstName : 'No User'}
;
+ };
+
+ let getByText;
+ await act(async () => {
+ ({ getByText } = render(
+
+
+
+ ));
+ });
+
+ await waitFor(() => expect(getByText('Jack')).toBeDefined());
+ });
+
+ it('should throw error if useGetAuth is used outside AuthProvider', () => {
+ const TestComponent = () => {
+ useGetAuth();
+ return Test
;
+ };
+
+ expect(() => render()).toThrow(
+ 'useGetAuth must be used within an AuthProvider'
+ );
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/index.test.tsx b/frontend/src/__test__/index.test.tsx
new file mode 100644
index 00000000..dba03fcd
--- /dev/null
+++ b/frontend/src/__test__/index.test.tsx
@@ -0,0 +1,53 @@
+import { describe, it, expect, vi } from 'vitest';
+import React from 'react';
+import { render } from '@testing-library/react';
+import { createRoot } from 'react-dom/client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { Provider } from 'react-redux';
+import { Amplify } from 'aws-amplify';
+import App from '../App';
+import store from '../store';
+import { ThemePreference } from '../utils/ThemePreference';
+import PaginationProvider from '../contexts/PaginationProvider';
+import { OpeningsSearchProvider } from '../contexts/search/OpeningsSearch';
+import { AuthProvider } from '../contexts/AuthProvider';
+import amplifyconfig from '../amplifyconfiguration';
+import { CookieStorage } from 'aws-amplify/utils';
+import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito';
+import { useLottie } from 'lottie-react';
+
+vi.mock('aws-amplify');
+vi.mock('aws-amplify/utils');
+vi.mock('aws-amplify/auth/cognito');
+vi.mock('@tanstack/react-query', () => ({
+ QueryClient: vi.fn(() => ({
+ defaultOptions: {
+ queries: {},
+ },
+ })),
+ QueryClientProvider: ({ children }) => {children}
,
+}));
+vi.mock('react-dom/client', () => ({
+ createRoot: vi.fn(() => ({
+ render: vi.fn(),
+ })),
+}));
+vi.mock('lottie-react', () => ({
+ useLottie: vi.fn(),
+}));
+
+describe('index.tsx', () => {
+ it('should initialize the app correctly', async () => {
+ (useLottie as vi.Mock).mockReturnValue({ View: Lottie Animation
});
+ const container = document.createElement('div');
+ container.id = 'root';
+ document.body.appendChild(container);
+
+ // Import the index file to execute its code
+ await import('../index');
+
+ expect(createRoot).toHaveBeenCalledWith(container);
+ expect(Amplify.configure).toHaveBeenCalledWith(amplifyconfig);
+ expect(cognitoUserPoolsTokenProvider.setKeyValueStorage).toHaveBeenCalledWith(expect.any(CookieStorage));
+ });
+});
diff --git a/frontend/src/__test__/routes/ProtectedRoute.test.tsx b/frontend/src/__test__/routes/ProtectedRoute.test.tsx
new file mode 100644
index 00000000..3783f05b
--- /dev/null
+++ b/frontend/src/__test__/routes/ProtectedRoute.test.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { MemoryRouter, Routes, Route } from 'react-router-dom';
+import { describe, it, expect, vi } from 'vitest';
+import ProtectedRoute from '../../routes/ProtectedRoute';
+import { useGetAuth } from '../../contexts/AuthProvider';
+import { Loading } from "@carbon/react";
+
+vi.mock('../../contexts/AuthProvider', () => ({
+ useGetAuth: vi.fn(),
+}));
+
+vi.mock('@carbon/react', () => ({
+ Loading: vi.fn(() => Loading...
),
+}));
+
+describe('ProtectedRoute', () => {
+ it('should render loading component when isLoading is true', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoading: true });
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Loading...')).toBeDefined();
+ });
+
+ it('should redirect to login when requireAuth is true and user is not logged in', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: false });
+
+ const { container } = render(
+
+
+ Login Page} />
+ } />
+
+
+ );
+
+ expect(container.innerHTML).toContain('Login Page');
+ });
+
+ it('should redirect to unauthorized when requiredRoles are not met', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: true, userRoles: ['user'] });
+
+ const { container } = render(
+
+
+ Unauthorized Page} />
+ } />
+
+
+ );
+
+ expect(container.innerHTML).toContain('Unauthorized Page');
+ });
+
+ it('should render child routes when all checks pass', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: true, userRoles: ['admin'] });
+
+ const { container } = render(
+
+
+ }>
+ Protected Content} />
+
+
+
+ );
+
+ expect(container.innerHTML).toContain('Protected Content');
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/screens/Dashboard.test.tsx b/frontend/src/__test__/screens/Dashboard.test.tsx
deleted file mode 100644
index bcf0e9b9..00000000
--- a/frontend/src/__test__/screens/Dashboard.test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import { describe, expect, it, vi } from 'vitest';
-import { render, screen } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
-import { Provider } from 'react-redux';
-import store from '../../store';
-import Dashboard from '../../screens/Dashboard';
-import * as redux from 'react-redux';
-
-const state = {
- userDetails: {
- user: {
- firstName: 'Test'
- },
- loading: false,
- error: null
- },
- selectedClientRoles: {
-
- }
-};
-
-vi.spyOn(redux, 'useSelector')
- .mockImplementation((callback) => callback(state));
-
-describe('Dashboard screen test cases', () => {
- it('should render the dashboard component', () => {
- render(
-
-
-
-
-
- );
-
- const headerElement = screen.getByText(/welcome to the SILVA/i);
- expect(headerElement).toBeDefined();
- })
-});
diff --git a/frontend/src/__test__/screens/DashboardRedirect.test.tsx b/frontend/src/__test__/screens/DashboardRedirect.test.tsx
new file mode 100644
index 00000000..14d032e7
--- /dev/null
+++ b/frontend/src/__test__/screens/DashboardRedirect.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { MemoryRouter, Navigate } from 'react-router-dom';
+import { describe, it, expect, vi } from 'vitest';
+import DashboardRedirect from '../../screens/DashboardRedirect';
+import { useGetAuth } from '../../contexts/AuthProvider';
+
+vi.mock('../../contexts/AuthProvider', () => ({
+ useGetAuth: vi.fn(),
+}));
+
+vi.mock('@carbon/react', () => ({
+ Loading: vi.fn(() => Loading...
),
+}));
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom');
+ return {
+ ...actual,
+ Navigate: vi.fn(({ to, replace }) => Navigate to {to} - {replace}
),
+ };
+});
+
+describe('DashboardRedirect', () => {
+ it('should render Loading component when user is not defined', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false });
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Loading...')).toBeDefined();
+ expect(Navigate).not.toHaveBeenCalled();
+ });
+
+ it('should navigate to /opening when user is defined', async () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: true });
+
+ const { getByText } = render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(getByText('Navigate to /opening -')).toBeDefined();
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/screens/ErrorHandling.test.tsx b/frontend/src/__test__/screens/ErrorHandling.test.tsx
new file mode 100644
index 00000000..dcfcf412
--- /dev/null
+++ b/frontend/src/__test__/screens/ErrorHandling.test.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { MemoryRouter, Navigate } from 'react-router-dom';
+import { describe, it, expect, vi } from 'vitest';
+import ErrorHandling from '../../screens/ErrorHandling';
+import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom');
+ return {
+ ...actual,
+ useRouteError: vi.fn(),
+ isRouteErrorResponse: vi.fn(),
+ Navigate: vi.fn(({ to, replace }) => Navigate to {to} - {replace}
),
+ };
+});
+
+describe('ErrorHandling', () => {
+ it('should navigate to / when error status is 401', () => {
+ (useRouteError as vi.Mock).mockReturnValue({ status: 401 });
+ (isRouteErrorResponse as vi.Mock).mockReturnValue(true);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Navigate to / -')).toBeDefined();
+ });
+
+ it('should display Unauthorized when error status is 403', () => {
+ (useRouteError as vi.Mock).mockReturnValue({ status: 403 });
+ (isRouteErrorResponse as vi.Mock).mockReturnValue(true);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Unauthorized')).toBeDefined();
+ });
+
+ it('should display Page Not Found when error status is 404', () => {
+ (useRouteError as vi.Mock).mockReturnValue({ status: 404 });
+ (isRouteErrorResponse as vi.Mock).mockReturnValue(true);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Page Not Found')).toBeDefined();
+ });
+
+ it('should display default error message for other types of errors', () => {
+ (useRouteError as vi.Mock).mockReturnValue({ status: 500 });
+ (isRouteErrorResponse as vi.Mock).mockReturnValue(true);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Oops! Something went wrong')).toBeDefined();
+ });
+
+ it('should display default error message when error is not a route error response', () => {
+ (useRouteError as vi.Mock).mockReturnValue(new Error('Test error'));
+ (isRouteErrorResponse as vi.Mock).mockReturnValue(false);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('Oops! Something went wrong')).toBeDefined();
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/screens/Landing.test.tsx b/frontend/src/__test__/screens/Landing.test.tsx
new file mode 100644
index 00000000..964cab22
--- /dev/null
+++ b/frontend/src/__test__/screens/Landing.test.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import Landing from '../../screens/Landing';
+import { useGetAuth } from '../../contexts/AuthProvider';
+import { useLottie } from 'lottie-react';
+
+vi.mock('../../contexts/AuthProvider', () => ({
+ useGetAuth: vi.fn(),
+}));
+
+vi.mock('lottie-react', () => ({
+ useLottie: vi.fn(),
+}));
+
+vi.mock('@carbon/react', () => ({
+ Button: ({ onClick, children, ...props }) => (
+
+ ),
+}));
+
+vi.mock('@carbon/icons-react', () => ({
+ Login: () => ,
+}));
+
+describe('Landing', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render the landing page with title and subtitle', () => {
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: vi.fn() });
+ (useLottie as vi.Mock).mockReturnValue({ View: Lottie Animation
});
+
+ const { getByTestId, getByText } = render();
+
+ expect(getByTestId('landing-title').textContent).toBe('Welcome to SILVA');
+ expect(getByTestId('landing-subtitle').textContent).toBe('Plan, report, and analyze your silviculture activities');
+ expect(getByText('Lottie Animation')).toBeDefined();
+ });
+
+ it('should call login with "idir" when Login with IDIR button is clicked', () => {
+ const mockLogin = vi.fn();
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: mockLogin });
+ (useLottie as vi.Mock).mockReturnValue({ View: Lottie Animation
});
+
+ const { getByTestId } = render();
+
+ fireEvent.click(getByTestId('landing-button__idir'));
+ expect(mockLogin).toHaveBeenCalledWith('idir');
+ });
+
+ it('should call login with "bceid" when Login with Business BCeID button is clicked', () => {
+ const mockLogin = vi.fn();
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: mockLogin });
+ (useLottie as vi.Mock).mockReturnValue({ View: Lottie Animation
});
+
+ const { getByTestId } = render();
+
+ fireEvent.click(getByTestId('landing-button__bceid'));
+ expect(mockLogin).toHaveBeenCalledWith('bceid');
+ });
+
+ it('should redirect to /dashboard if user is already logged in', async () => {
+ const mockLogin = vi.fn();
+ (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: true, login: mockLogin });
+ (useLottie as vi.Mock).mockReturnValue({ View: Lottie Animation
});
+
+ const originalLocation = window.location;
+ delete window.location;
+ window.location = { href: '' };
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(window.location.href).toContain('/dashboard');
+ });
+
+ // Restore original window.location
+ window.location = originalLocation;
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/services/AuthService.test.ts b/frontend/src/__test__/services/AuthService.test.ts
new file mode 100644
index 00000000..12c8b16c
--- /dev/null
+++ b/frontend/src/__test__/services/AuthService.test.ts
@@ -0,0 +1,130 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { parseToken, getAuthIdToken } from '../../services/AuthService';
+import { formatRolesArray } from '../../utils/famUtils';
+import { JWT } from '../../types/amplify';
+
+vi.mock('../../utils/famUtils', () => ({
+ formatRolesArray: vi.fn(),
+}));
+
+describe('AuthService', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('parseToken', () => {
+ it('should return undefined if idToken is undefined', () => {
+ const result = parseToken(undefined);
+ expect(result).toBeUndefined();
+ });
+
+ it('should set authIdToken and return parsed user data', () => {
+ const mockToken: JWT = {
+ payload: {
+ "cognito:groups": [
+ "group1",
+ "group2"
+ ],
+ "preferred_username": "b5ecdb094dfb4149a6a8445a01a96bf0@idir",
+ "custom:idp_user_id": "B5ECDB094DFB4149A6A8445A01A96BF0",
+ "custom:idp_username": "JRYAN",
+ "custom:idp_display_name": "Ryan, Jack CIA:IN",
+ "email": "jack.ryan@gov.bc.ca",
+ "email_verified": false,
+ "custom:idp_name": "idir",
+ "given_name": "Jack",
+ "name": "Jack Ryan",
+ "family_name": "Ryan",
+ exp: 1234567890
+ },
+ toString: () => 'mockTokenString',
+ };
+
+ (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]);
+
+ const result = parseToken(mockToken);
+
+ expect(getAuthIdToken()).toBe('mockTokenString');
+ expect(result).toEqual({
+ userName: 'JRYAN',
+ displayName: "Ryan, Jack CIA:IN",
+ email: 'jack.ryan@gov.bc.ca',
+ idpProvider: 'IDIR',
+ clientRoles: [{ role: 'role1' }, { role: 'role2' }],
+ exp: 1234567890,
+ firstName: 'Jack',
+ lastName: 'Ryan',
+ providerUsername: 'IDIR\\JRYAN',
+ });
+ });
+
+ it('should handle displayName without comma correctly', () => {
+ const mockToken: JWT = {
+ payload: {
+ "cognito:groups": [
+ "group1",
+ "group2"
+ ],
+ "preferred_username": "b5ecdb094dfb4149a6a8445a01a96bf0@idir",
+ "custom:idp_user_id": "B5ECDB094DFB4149A6A8445A01A96BF0",
+ "custom:idp_username": "JRYAN",
+ "custom:idp_display_name": "Jack Ryan",
+ "email": "jack.ryan@gov.bc.ca",
+ "email_verified": false,
+ "custom:idp_name": "idir",
+ "given_name": "Jack",
+ "name": "Jack Ryan",
+ "family_name": "Ryan",
+ exp: 1234567890
+ },
+ toString: () => 'mockTokenString',
+ };
+
+ (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]);
+
+ const result = parseToken(mockToken);
+
+ expect(getAuthIdToken()).toBe('mockTokenString');
+ expect(result).toEqual({
+ userName: 'JRYAN',
+ displayName: 'Jack Ryan',
+ email: 'jack.ryan@gov.bc.ca',
+ idpProvider: 'IDIR',
+ clientRoles: [{ role: 'role1' }, { role: 'role2' }],
+ exp: 1234567890,
+ firstName: 'Jack',
+ lastName: 'Ryan',
+ providerUsername: 'IDIR\\JRYAN',
+ });
+ });
+
+ it('should handle missing identities correctly', () => {
+ const mockToken: JWT = {
+ payload: {
+ 'custom:idp_display_name': 'Doe, John',
+ 'custom:idp_username': 'johndoe',
+ 'email': 'john.doe@example.com',
+ exp: 1234567890,
+ },
+ toString: () => 'mockTokenString',
+ };
+
+ (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]);
+
+ const result = parseToken(mockToken);
+
+ expect(getAuthIdToken()).toBe('mockTokenString');
+ expect(result).toEqual({
+ userName: 'johndoe',
+ displayName: 'Doe, John',
+ email: 'john.doe@example.com',
+ idpProvider: '',
+ clientRoles: [{ role: 'role1' }, { role: 'role2' }],
+ exp: 1234567890,
+ firstName: 'John',
+ lastName: 'Doe',
+ providerUsername: '\\johndoe',
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/utils/famUtils.test.ts b/frontend/src/__test__/utils/famUtils.test.ts
new file mode 100644
index 00000000..6dcc807d
--- /dev/null
+++ b/frontend/src/__test__/utils/famUtils.test.ts
@@ -0,0 +1,76 @@
+import { describe, it, expect } from 'vitest';
+import { formatRolesArray, extractGroups } from '../../utils/famUtils';
+
+describe('famUtils', () => {
+ describe('formatRolesArray', () => {
+ it('should return an empty array if decodedIdToken is undefined', () => {
+ const result = formatRolesArray(undefined);
+ expect(result).toEqual([]);
+ });
+
+ it('should return an empty array if decodedIdToken does not contain cognito:groups', () => {
+ const decodedIdToken = {};
+ const result = formatRolesArray(decodedIdToken);
+ expect(result).toEqual([]);
+ });
+
+ it('should return formatted roles array if decodedIdToken contains cognito:groups', () => {
+ const decodedIdToken = {
+ 'cognito:groups': ['admin_123', 'user_456', 'editor_123']
+ };
+ const result = formatRolesArray(decodedIdToken);
+ expect(result).toEqual([
+ {
+ clientId: '123',
+ roles: ['admin', 'editor'],
+ clientName: 'Client Number 123'
+ },
+ {
+ clientId: '456',
+ roles: ['user'],
+ clientName: 'Client Number 456'
+ }
+ ]);
+ });
+
+ it('should handle groups without underscores correctly', () => {
+ const decodedIdToken = {
+ 'cognito:groups': ['admin', 'user_456', 'editor_123']
+ };
+ const result = formatRolesArray(decodedIdToken);
+ expect(result).toEqual([
+ {
+ clientId: '123',
+ roles: ['editor'],
+ clientName: 'Client Number 123'
+ },
+ {
+ clientId: '456',
+ roles: ['user'],
+ clientName: 'Client Number 456'
+ }
+ ]);
+ });
+ });
+
+ describe('extractGroups', () => {
+ it('should return an empty array if decodedIdToken is undefined', () => {
+ const result = extractGroups(undefined);
+ expect(result).toEqual([]);
+ });
+
+ it('should return an empty array if decodedIdToken does not contain cognito:groups', () => {
+ const decodedIdToken = {};
+ const result = extractGroups(decodedIdToken);
+ expect(result).toEqual([]);
+ });
+
+ it('should return groups array if decodedIdToken contains cognito:groups', () => {
+ const decodedIdToken = {
+ 'cognito:groups': ['admin', 'user', 'editor']
+ };
+ const result = extractGroups(decodedIdToken);
+ expect(result).toEqual(['admin', 'user', 'editor']);
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/actions/userAction.ts b/frontend/src/actions/userAction.ts
index 03b74b06..87d6369d 100644
--- a/frontend/src/actions/userAction.ts
+++ b/frontend/src/actions/userAction.ts
@@ -4,9 +4,9 @@ import {
USER_DETAILS_FAIL,
SET_CLIENT_ROLES
} from '../constants/userConstants';
-import { isCurrentAuthUser } from '../services/AuthService';
import { AppDispatch } from '../store';
import { UserClientRolesType } from '../types/UserRoleType';
+import { useGetAuth } from '../contexts/AuthProvider';
const FAM_LOGIN_USER = 'famLoginUser';
@@ -15,15 +15,15 @@ export const getUserDetails = () => async (dispatch: AppDispatch) => {
dispatch({
type: USER_DETAILS_REQUEST
});
- //first call the isCurrent and only after that extract the JSON
- const data = await isCurrentAuthUser();
+ //first call the isCurrent and only after that extract the JSON
+ const { isLoggedIn } = useGetAuth();
const userJSON = localStorage.getItem(FAM_LOGIN_USER); // Retrieve the JSON string from local storage
const user = userJSON ? JSON.parse(userJSON) : null; // Parse the JSON string to a JavaScript object
dispatch({
type: USER_DETAILS_SUCCESS,
- payload: { ...user, isLoggedIn: data }
+ payload: { ...user, isLoggedIn }
});
} catch (error) {
dispatch({
diff --git a/frontend/src/amplifyconfiguration.ts b/frontend/src/amplifyconfiguration.ts
index 709002c5..31a482f0 100644
--- a/frontend/src/amplifyconfiguration.ts
+++ b/frontend/src/amplifyconfiguration.ts
@@ -17,8 +17,8 @@ const verificationMethods: verificationMethodsType = 'code';
const amplifyconfig = {
Auth: {
Cognito: {
- userPoolId: env.VITE_USER_POOLS_ID ?? "ca-central-1_t2HSZBHur",
- userPoolClientId: env.VITE_USER_POOLS_WEB_CLIENT_ID ?? "70a2am185rie10r78b0ugcs1mm",
+ userPoolId: env.VITE_USER_POOLS_ID,
+ userPoolClientId: env.VITE_USER_POOLS_WEB_CLIENT_ID,
signUpVerificationMethod: verificationMethods, // 'code' | 'link'
loginWith: {
oauth: {
diff --git a/frontend/src/components/MyProfile/index.tsx b/frontend/src/components/MyProfile/index.tsx
index e58ab25d..7cf48f55 100644
--- a/frontend/src/components/MyProfile/index.tsx
+++ b/frontend/src/components/MyProfile/index.tsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
-import { useSelector } from 'react-redux';
import { SideNavLink } from '@carbon/react';
import { Asleep, Light, UserFollow } from '@carbon/icons-react';
import AvatarImage from '../AvatarImage';
@@ -7,13 +6,12 @@ import { useThemePreference } from '../../utils/ThemePreference';
import PanelSectionName from '../PanelSectionName';
import OrganizationSelection from '../OrganizationSelection';
import './MyProfile.scss';
-import { logout } from '../../services/AuthService';
-import { RootState } from '../../store';
+import { useGetAuth } from '../../contexts/AuthProvider';
const MyProfile = () => {
const { theme, setTheme } = useThemePreference();
- const userDetails = useSelector((state: RootState) => state.userDetails)
const [goTo, setGoTo] = useState(false);
+ const { logout, user: authUser } = useGetAuth();
const changeTheme = () => {
if (theme === 'g10') {
@@ -36,12 +34,12 @@ const MyProfile = () => {
<>
-
{`${userDetails.user.firstName} ${userDetails.user.lastName}`}
-
{`IDIR: ${userDetails.user.userName}`}
-
{`Email:${userDetails.user.email}`}
+
{`${authUser?.firstName} ${authUser?.lastName}`}
+
{`IDIR: ${authUser?.userName}`}
+
{`Email:${authUser?.email}`}
diff --git a/frontend/src/components/OrganizationSelection/index.tsx b/frontend/src/components/OrganizationSelection/index.tsx
index d0c44062..b33ed7fb 100644
--- a/frontend/src/components/OrganizationSelection/index.tsx
+++ b/frontend/src/components/OrganizationSelection/index.tsx
@@ -1,236 +1,13 @@
-import React, { useState } from 'react';
-import {
- FlexGrid, Row, Column,
- ButtonSkeleton, Search, Button,
- ContainedListItem
-} from '@carbon/react';
-import { ArrowRight } from '@carbon/icons-react';
-import { useQueries, useQueryClient } from '@tanstack/react-query';
-import { logout } from '../../services/AuthService';
-import { setSelectedClientRoles } from '../../actions/selectedClientRolesActions';
-import { getForestClientByNumberOrAcronym } from '../../services/TestService';
-import { THREE_HALF_HOURS, THREE_HOURS } from '../../config/TimeUnits';
-import { UserClientRolesType } from '../../types/UserRoleType';
-import { ForestClientType } from '../../types/ForestClientTypes/ForestClientType';
-import EmptySection from '../EmptySection';
-
-import { MIN_CLIENTS_SHOW_SEARCH, TEXT } from './constants';
+import { FlexGrid } from '@carbon/react';
import { RoleSelectionProps } from './definitions';
-import OrganizationItem from './OrganizationItem';
-
-const SELECTED_CLIENT_ROLES = 'selectedClientRoles';
-
import './styles.scss';
-import { AppDispatch, RootState } from '../../store';
-import { useDispatch, useSelector } from 'react-redux';
-
const OrganizationSelection = ({ simpleView }: RoleSelectionProps) => {
- const dispatch = useDispatch();
- const userDetails = useSelector((state: RootState) => state.userDetails);
-
- const user = userDetails.user;
- const selectedClientRoles = useSelector((state: RootState) => state.selectedClientRoles);
-
- const [matchedClients, setMatchedClients] = useState([]);
- const [searchTerm, setSearchTerm] = useState('');
- const [clientRolesToSet, setClientRolesToSet] = useState(selectedClientRoles);
-
- useQueries({
- queries: user?.clientRoles?.map((clientRole:UserClientRolesType) => ({
- queryKey: ['role', 'forest-clients', clientRole.clientId],
- queryFn: () => getForestClientByNumberOrAcronym(clientRole.clientId),
- staleTime: THREE_HOURS,
- cacheTime: THREE_HALF_HOURS,
- refetchOnReconnect: false
- })) ?? []
- });
-
- const qc = useQueryClient();
-
- const filterClientsByValue = (value: string) => {
- const forestClientsQueriesData = qc.getQueriesData({ queryKey: ['role', 'forest-clients'] });
-
- const forestClients = forestClientsQueriesData.map((qData) => (
- qData.at(1) as ForestClientType
- ));
-
- const loweredSearchTerm = value.toLowerCase();
-
- const foundByName = forestClients
- .filter((fc) => (fc.clientName.toLowerCase().includes(loweredSearchTerm)));
-
- const foundById = forestClients
- .filter((fc) => (fc.clientNumber.includes(loweredSearchTerm)));
-
- const foundCombined = foundByName.concat(foundById);
-
- const foundIds = foundCombined.map((fc) => fc.clientNumber);
-
- setSearchTerm(value);
- setMatchedClients(foundIds);
- };
-
- const setSelectedClientRolesHandler = (clientId: string, clientName?: string) => {
- if (clientId) {
- const found = user?.clientRoles?.find((uClientRole: UserClientRolesType) => (
- uClientRole.clientId === clientId
- ));
- if (found) {
- const toSet: UserClientRolesType = {
- ...found,
- clientName
- };
- setClientRolesToSet(toSet);
- if (simpleView) {
- localStorage.setItem(SELECTED_CLIENT_ROLES, JSON.stringify(toSet))
- dispatch(setSelectedClientRoles(toSet));
- }
- }
- }
- };
-
- const continueToDashboard = () => {
- if (clientRolesToSet) {
- localStorage.setItem(SELECTED_CLIENT_ROLES, JSON.stringify(clientRolesToSet))
- dispatch(setSelectedClientRoles(clientRolesToSet));
- }
- };
-
- const renderOrgItem = (clientRole: UserClientRolesType) => {
- const queryKey = ['role', 'forest-clients', clientRole.clientId];
- const queryState = qc.getQueryState(queryKey);
- const queryData: ForestClientType | undefined = qc.getQueryData(queryKey);
-
- //then it would automatically be loading, but the loading is not working still need to determine
- if (queryState?.status !== 'success' && queryState?.status !== 'error') {
- return (
-
-
-
-
-
- );
- }
-
- if (
- (
- matchedClients.length === 0
- && searchTerm === ''
- )
- || matchedClients.includes(clientRole.clientId)
- ) {
- return (
- setSelectedClientRolesHandler(clientRole.clientId, queryData?.clientName)}
- >
-
-
- );
- }
-
- return null;
- };
-
- const renderListSection = () => {
- if (user?.clientRoles.length === 0) {
- return (
-
-
-
- );
- }
-
- if (searchTerm.length > 0 && matchedClients.length === 0) {
- return (
-
-
- {TEXT.emptySearch}
-
- )}
- />
-
- );
- }
-
- return (
-
-
- {
- user?.clientRoles
- .map((clientRole:any) => (
- renderOrgItem(clientRole)
- ))
- }
-
-
- );
- };
+
return (
-
- {
- user!.clientRoles.length > MIN_CLIENTS_SHOW_SEARCH
- ? (
-
-
- ) => filterClientsByValue(e.target.value)
- }
- />
-
-
- ) : null
- }
-
- {
- renderListSection()
- }
-
- {simpleView?null:(
-
-
-
-
-
-
-
-
- )}
+
);
};
-export default OrganizationSelection;
+export default OrganizationSelection;
\ No newline at end of file
diff --git a/frontend/src/contexts/AuthProvider.tsx b/frontend/src/contexts/AuthProvider.tsx
new file mode 100644
index 00000000..72517541
--- /dev/null
+++ b/frontend/src/contexts/AuthProvider.tsx
@@ -0,0 +1,133 @@
+import React, { createContext, useState, useContext, useEffect, useMemo, ReactNode } from 'react';
+import { fetchAuthSession, signInWithRedirect, signOut } from "aws-amplify/auth";
+import { parseToken, FamLoginUser } from "../services/AuthService";
+import { extractGroups } from '../utils/famUtils';
+import { env } from '../env';
+import { JWT } from '../types/amplify';
+
+// 1. Define an interface for the context value
+interface AuthContextType {
+ user: FamLoginUser | undefined;
+ userRoles: string[] | undefined;
+ isLoggedIn: boolean;
+ isLoading: boolean;
+ login: (provider: string) => void;
+ logout: () => void;
+
+}
+
+// 2. Define an interface for the provider's props
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+// 3. Create the context with a default value of `undefined`
+const AuthContext = createContext(undefined);
+
+// 4. Create the AuthProvider component with explicit typing
+export const AuthProvider: React.FC = ({ children }) => {
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [user, setUser] = useState(undefined);
+ const [userRoles, setUserRoles] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const appEnv = env.VITE_ZONE ?? 'DEV';
+
+
+ useEffect(() => {
+ const checkUser = async () => {
+ try{
+ const idToken = await loadUserToken();
+ setIsLoggedIn(!!idToken);
+ setIsLoading(false);
+ if(idToken){
+ setUser(parseToken(idToken));
+ setUserRoles(extractGroups(idToken?.payload));
+ }
+ }catch(error){
+ setIsLoggedIn(false);
+ setUser(parseToken(undefined));
+ setIsLoading(false);
+ }
+ };
+ checkUser();
+ }, []);
+
+ const login = async (provider: string) => {
+ const envProvider = (provider.localeCompare('idir') === 0)
+ ?`${(appEnv).toLocaleUpperCase()}-IDIR`
+ : `${(appEnv).toLocaleUpperCase()}-BCEIDBUSINESS`;
+
+ signInWithRedirect({
+ provider: { custom: envProvider.toUpperCase() }
+ });
+ };
+
+ const logout = async () => {
+ await signOut();
+ setIsLoggedIn(false);
+ window.location.href = '/'; // Optional redirect after logout
+ };
+
+ const contextValue = useMemo(() => ({
+ user,
+ userRoles,
+ isLoggedIn,
+ isLoading,
+ login,
+ logout
+ }), [user, userRoles, isLoggedIn, isLoading]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+// This is a helper hook to use the Auth context more easily
+// 5. Create a custom hook to consume the context safely
+export const useGetAuth = (): AuthContextType => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useGetAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+const loadUserToken = async () : Promise => {
+ if(env.NODE_ENV !== 'test'){
+ const {idToken} = (await fetchAuthSession()).tokens ?? {};
+ return Promise.resolve(idToken);
+ } else {
+ // This is for test only
+ const token = getUserTokenFromCookie();
+ if (token) {
+ const jwtBody = token
+ ? JSON.parse(atob(token.split(".")[1]))
+ : null;
+ return Promise.resolve({ payload: jwtBody });
+ } else {
+ return Promise.reject(new Error("No token found"));
+ }
+ }
+};
+
+const getUserTokenFromCookie = (): string|undefined => {
+ const baseCookieName = `CognitoIdentityServiceProvider.${env.VITE_USER_POOLS_WEB_CLIENT_ID}`;
+ const userId = encodeURIComponent(getCookie(`${baseCookieName}.LastAuthUser`));
+ if (userId) {
+ const idTokenCookieName = `${baseCookieName}.${userId}.idToken`;
+ const idToken = getCookie(idTokenCookieName);
+ return idToken;
+ } else {
+ return undefined;
+ }
+};
+
+const getCookie = (name: string): string => {
+ const cookie = document.cookie
+ .split(";")
+ .find((cookieValue) => cookieValue.trim().startsWith(name));
+ return cookie ? cookie.split("=")[1] : "";
+};
\ No newline at end of file
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 5d1bf9bf..48e9d8d6 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,6 +1,5 @@
window.global ||= window;
import React from 'react';
-import ReactDOM from 'react-dom';
import './index.css';
import { ClassPrefix } from '@carbon/react';
import { Provider } from 'react-redux'
@@ -12,6 +11,11 @@ import PaginationProvider from './contexts/PaginationProvider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { OpeningsSearchProvider } from './contexts/search/OpeningsSearch';
+import { Amplify } from 'aws-amplify';
+import amplifyconfig from './amplifyconfiguration';
+import { CookieStorage } from 'aws-amplify/utils';
+import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito';
+import { AuthProvider } from './contexts/AuthProvider';
const container: HTMLElement | null = document.getElementById('root');
if (container) {
@@ -41,19 +45,25 @@ if (container) {
}
});
+ Amplify.configure(amplifyconfig);
+ cognitoUserPoolsTokenProvider.setKeyValueStorage(new CookieStorage());
+
+
root.render(
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/PostLoginRoute.tsx b/frontend/src/routes/PostLoginRoute.tsx
deleted file mode 100644
index dc784805..00000000
--- a/frontend/src/routes/PostLoginRoute.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { useEffect } from 'react';
-import { Navigate } from 'react-router-dom';
-import { getUserDetails } from '../actions/userAction';
-import { Loading } from "@carbon/react";
-import { RootState } from '../store';
-import { useDispatch, useSelector } from 'react-redux';
-
-interface IProps {
- children: JSX.Element;
-}
-
-const PostLoginRoute = ({ children }: IProps): JSX.Element => {
- const userDetails = useSelector((state: RootState) => state.userDetails)
- const { loading, error, user } = userDetails
- const { pathname } = window.location;
- const encodedUrl = encodeURI(`/?page=${pathname}`);
-
- const dispatch:any = useDispatch();
- useEffect(()=>{
- dispatch(getUserDetails())
- },[dispatch])
-
- return (
- loading ? (
-
- ):
- error? (
- Error
- ):(
- (() => {
- if(user?.isLoggedIn === true){
- return children;
- }
- else if(user?.isLoggedIn === false){
- return ;
- }
-
- return <>>
- })()
- )
- );
-};
-
-export default PostLoginRoute ;
\ No newline at end of file
diff --git a/frontend/src/routes/ProtectedRoute.tsx b/frontend/src/routes/ProtectedRoute.tsx
index 82f7b7b8..d719c0e4 100644
--- a/frontend/src/routes/ProtectedRoute.tsx
+++ b/frontend/src/routes/ProtectedRoute.tsx
@@ -1,33 +1,37 @@
import React from 'react';
-import { Navigate } from 'react-router-dom';
-import { RootState } from '../store';
-import { useSelector } from 'react-redux';
+import { Navigate, Outlet } from 'react-router-dom';
+import { useGetAuth } from '../contexts/AuthProvider';
+import { Loading } from "@carbon/react";
-interface IProps {
- children: JSX.Element;
+interface ProtectedRouteProps {
+ requireAuth?: boolean;
+ requiredRoles?: string[];
+ redirectTo?: string;
}
-const ProtectedRoute = ({ children }: IProps): JSX.Element => {
- const userDetails = useSelector((state: RootState) => state.userDetails);
- const { error, user } = userDetails;
- const { pathname } = window.location;
- const encodedUrl = encodeURI(`/?page=${pathname}`);
- return (
- error? (
- Error
- ):(
- (() => {
- if(user?.isLoggedIn){
- return children;
- }
- else if(user?.isLoggedIn === false){
- return ;
- }
+const ProtectedRoute: React.FC = ({
+ requireAuth = false,
+ requiredRoles = [],
+ redirectTo = '/'
+}) => {
+ const { isLoggedIn, isLoading, userRoles } = useGetAuth();
- return <>Sorry >;
- })()
- )
- );
+ if(isLoading) {
+ return ;
+ }
+
+ // 1. If authentication is required and the user is not logged in, redirect to login
+ if (requireAuth && !isLoggedIn) {
+ return ;
+ }
+
+ // 2. If specific roles are required and user does not have them, redirect to unauthorized page
+ if (requiredRoles.length > 0 && !requiredRoles.some(role => userRoles?.includes(role))) {
+ return ;
+ }
+
+ // 3. If all checks pass, render child routes
+ return ;
};
export default ProtectedRoute;
diff --git a/frontend/src/screens/Dashboard/Dashboard.scss b/frontend/src/screens/Dashboard/Dashboard.scss
deleted file mode 100644
index e5709c3c..00000000
--- a/frontend/src/screens/Dashboard/Dashboard.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.landing-grid{
- padding-right: 0;
- height: 100vh;
- }
\ No newline at end of file
diff --git a/frontend/src/screens/Dashboard/index.tsx b/frontend/src/screens/Dashboard/index.tsx
deleted file mode 100644
index f7cee077..00000000
--- a/frontend/src/screens/Dashboard/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { useSelector } from "react-redux";
-import { RootState } from "../../store";
-
-const Dashboard: React.FC = () => {
- const userDetails = useSelector((state: RootState) => state.userDetails);
- const selectedClientRoles = useSelector((state: RootState) => state.selectedClientRoles);
- const { user } = userDetails;
-
- return (
- <>
-
-
- Hello
- {user.firstName+" "+user.lastName},
- welcome to the SILVA portal. You are logged in with the client
- {selectedClientRoles?.clientName}!!
-
- >
- );
-};
-
-export default Dashboard;
diff --git a/frontend/src/screens/DashboardRedirect/index.tsx b/frontend/src/screens/DashboardRedirect/index.tsx
index a2f0ffb5..c8e32c9f 100644
--- a/frontend/src/screens/DashboardRedirect/index.tsx
+++ b/frontend/src/screens/DashboardRedirect/index.tsx
@@ -1,32 +1,13 @@
import React from "react";
-import { useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import LoginOrgSelection from "../../views/LoginOrgSelection";
-import SideLayout from "../../layouts/SideLayout";
-import Opening from "../Opening";
-import { RootState } from "../../store";
-import { useSelector } from "react-redux";
+import { Navigate } from "react-router-dom";
+import { Loading } from "@carbon/react";
+import { useGetAuth } from "../../contexts/AuthProvider";
const DashboardRedirect: React.FC = () => {
- const userDetails = useSelector((state: RootState) => state.userDetails);
- const { user } = userDetails;
-
- const navigate = useNavigate();
-
- // Redirect logic based on selectedClientRoles existence
- useEffect(() => {
- if (user) {
- navigate("/opening");
- }
- }, [user]);
-
+ const { isLoggedIn } = useGetAuth();
return (
<>
- {user ? (
- } />
- ) : (
-
- )}
+ {isLoggedIn ? : }
>
);
};
diff --git a/frontend/src/screens/ErrorHandling/index.tsx b/frontend/src/screens/ErrorHandling/index.tsx
new file mode 100644
index 00000000..5b9d74e1
--- /dev/null
+++ b/frontend/src/screens/ErrorHandling/index.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { isRouteErrorResponse, useRouteError, Navigate } from 'react-router-dom';
+
+const ErrorHandling: React.FC = () => {
+ const error = useRouteError();
+
+ console.log('ErrorHandling',error);
+
+ if (isRouteErrorResponse(error)) {
+ if (error.status === 401) {
+ return
+ } else if (error.status === 403) {
+ return Unauthorized
;
+ } else if (error.status === 404) {
+ return Page Not Found
;
+ }
+ }
+
+ // Default error message for other types of errors
+ return Oops! Something went wrong
;
+};
+
+export default ErrorHandling;
diff --git a/frontend/src/screens/Landing/index.tsx b/frontend/src/screens/Landing/index.tsx
index df22534e..22d0668f 100644
--- a/frontend/src/screens/Landing/index.tsx
+++ b/frontend/src/screens/Landing/index.tsx
@@ -1,35 +1,29 @@
-import React, { useCallback } from "react";
+import React from "react";
import BCGovLogo from "../../components/BCGovLogo";
-import { Button, InlineNotification } from "@carbon/react";
+import { Button } from "@carbon/react";
import { Login } from '@carbon/icons-react';
-import { signIn } from "../../services/AuthService";
import './Landing.scss';
import '../../custom.scss';
import { useLottie } from "lottie-react";
import silvaLottie from "../../assets/lotties/silva-logo-lottie-1.json"
-import ThemeToggle from "../../components/ThemeToggle";
-import { useNavigate } from "react-router-dom";
+import { useGetAuth } from "../../contexts/AuthProvider";
const Landing: React.FC = () => {
+
+ const { login, isLoggedIn } = useGetAuth();
+
// Adding the Lottie Loader and loading the View for lottie with initial options
const options = {
animationData: silvaLottie,
loop: true
};
const { View } = useLottie(options);
- const navigate = useNavigate();
- const login = useCallback(async (provider: string) => {
- try {
- await signIn(provider)
- } catch(e) {
- if (e && typeof e === "object" && "message" in e) {
- const messageError = e.message as string;
- if (messageError === 'There is already a signed in user.') {
- navigate('/dashboard');
- }
- }
- }
- }, []);
+
+ // If the user is already logged in, redirect to the dashboard
+ if (isLoggedIn) {
+ window.location.href = '/dashboard';
+ }
+
return (
<>
@@ -61,7 +55,7 @@ const Landing: React.FC = () => {